adopt: état déployé sur M3U96b adopté comme source de vérité (remote-first, 2026-10-30)
200 changed files +20,001 −0
added
.env.example
+34 −0
@@ -0,0 +1,34 @@ | |||||
| 1 | +# AutoScholar.ai — environment reference. No real values here. Copy to .env (never committed). | ||||
| 2 | + | ||||
| 3 | +# --- Control plane (apps/web + services/scheduler) --- | ||||
| 4 | +NODE_ENV=development # development | production | test | ||||
| 5 | +APP_BASE_URL=http://localhost:8470 # canonical public base URL (https://autoscholar.ai in production) | ||||
| 6 | +PORT=8470 # web port | ||||
| 7 | +DATABASE_URL=postgresql://localhost:5432/autoscholar # PostgreSQL 17 connection string | ||||
| 8 | +ARTIFACT_ROOT=./data/artifacts # filesystem artifact store root (private); public promotions live under <root>/public | ||||
| 9 | +SESSION_SECRET= # >=32 random bytes, hex; signs session cookies and CSRF tokens | ||||
| 10 | +ADMIN_BOOTSTRAP_USERNAME=admin # used only when admin_users is empty | ||||
| 11 | +ADMIN_BOOTSTRAP_PASSWORD= # used only when admin_users is empty; forced rotation at first login; never logged | ||||
| 12 | +WORKER_ENROLLMENT_SECRET= # one-time secret a new MacLustr worker presents to enroll; rotate after use | ||||
| 13 | +INTERNAL_API_ALLOWED_CIDRS=127.0.0.1/32,192.168.2.0/24 # networks allowed to reach /api/internal/* | ||||
| 14 | +TRUST_PROXY=0 # 1 behind the MacLustr Tunnel (Caddy sets X-Forwarded-For) | ||||
| 15 | +LOG_LEVEL=info | ||||
| 16 | +SCHEDULER_TICK_MS=5000 | ||||
| 17 | +LEASE_TTL_SECONDS=120 | ||||
| 18 | +DEFAULT_MAX_BUDGET_USD=25 | ||||
| 19 | +DEFAULT_MAX_RUNTIME_MINUTES=240 | ||||
| 20 | +PUBLISHING_ENABLED=1 # emergency switch: 0 halts all automatic publication | ||||
| 21 | +DISPATCH_ENABLED=1 # emergency switch: 0 pauses all new dispatches | ||||
| 22 | +STAGING_MODE=0 # 1 = staging/demo content allowed, site marked noindex | ||||
| 23 | + | ||||
| 24 | +# --- Worker (services/worker, runs as the dedicated low-privilege user on a MacLustr node) --- | ||||
| 25 | +CONTROL_PLANE_URL=http://192.168.2.85:8470 # LAN address of the control plane (never the public domain) | ||||
| 26 | +WORKER_NAME= # e.g. M4M64c | ||||
| 27 | +WORKER_TOKEN= # issued at enrollment (or WORKER_ENROLLMENT_SECRET for first enrollment) | ||||
| 28 | +WORKER_ROOT=/var/autoscholar # run workspaces live in <root>/runs/<run-id> | ||||
| 29 | +WORKER_MAX_CONCURRENT=1 | ||||
| 30 | +ANTHROPIC_API_KEY= # server-side only; research runner subprocess env; never sent to the browser | ||||
| 31 | +ANTHROPIC_MODEL=claude-opus-5-5 # default research model (overridable per run) | ||||
| 32 | +RESEARCH_ALLOWED_DOMAINS= # extra egress hostnames allowed inside the sandbox (comma separated) | ||||
| 33 | +TECTONIC_BIN=/opt/homebrew/bin/tectonic | ||||
| 34 | +WORKER_MODE=real # real | simulated (simulated is for tests only; results are flagged synthetic) | ||||
added
.gitignore
+18 −0
@@ -0,0 +1,18 @@ | |||||
| 1 | +node_modules/ | ||||
| 2 | +dist/ | ||||
| 3 | +.next/ | ||||
| 4 | +out/ | ||||
| 5 | +*.tsbuildinfo | ||||
| 6 | +.env | ||||
| 7 | +.env.local | ||||
| 8 | +.env.*.local | ||||
| 9 | +coverage/ | ||||
| 10 | +test-results/ | ||||
| 11 | +playwright-report/ | ||||
| 12 | +.DS_Store | ||||
| 13 | +/data/ | ||||
| 14 | +/var/ | ||||
| 15 | +*.log | ||||
| 16 | +.turbo/ | ||||
| 17 | +research/templates/latex/_build/ | ||||
| 18 | +dist-bundle/ | ||||
added
.npmrc
+2 −0
@@ -0,0 +1,2 @@ | |||||
| 1 | +verify-deps-before-run=false | ||||
| 2 | +confirm-modules-purge=false | ||||
added
README.md
+14 −0
@@ -0,0 +1,14 @@ | |||||
| 1 | +# AutoScholar.ai — Autonomous Research Foundry | ||||
| 2 | + | ||||
| 3 | +From research instructions to verifiable working papers. Research computed on MacLustr. | ||||
| 4 | + | ||||
| 5 | +- `apps/web` — public research library, private admin, HTTP APIs (Next.js) | ||||
| 6 | +- `services/scheduler` — durable queue, leases, stage state machine, quality gates, publication | ||||
| 7 | +- `services/worker` — MacLustr worker daemon (runs as a dedicated low-privilege user) | ||||
| 8 | +- `services/research-runner` — Claude Agent SDK research executor (sandboxed, budgeted) | ||||
| 9 | +- `services/qa` — deterministic quality gates · `services/publisher` — transactional publication | ||||
| 10 | +- `packages/*` — db (Drizzle/PostgreSQL), schemas (zod), auth, storage, research-contract, maclustr-adapter, observability, ui | ||||
| 11 | +- `research/templates` — starter research `CLAUDE.md` files and the LaTeX class | ||||
| 12 | +- `docs/` — INFRA_DISCOVERY, DECISIONS, IMPLEMENTATION_STATUS, TEST_REPORT, SECURITY, OPERATIONS, RESEARCH_INSTRUCTIONS_GUIDE, LAUNCH_REPORT | ||||
| 13 | + | ||||
| 14 | +Local development: `cp .env.example .env`, set `DATABASE_URL`, `SESSION_SECRET`, `ADMIN_BOOTSTRAP_PASSWORD`; then `pnpm install && pnpm db:migrate && pnpm dev` (web on :8470) and `pnpm dev:scheduler`. | ||||
added
apps/web/AGENTS.md
+9 −0
@@ -0,0 +1,9 @@ | |||||
| 1 | +<!-- BEGIN:nextjs-agent-rules --> | ||||
| 2 | + | ||||
| 3 | +## This is NOT the Next.js you know | ||||
| 4 | + | ||||
| 5 | +This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices. | ||||
| 6 | + | ||||
| 7 | +This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean. | ||||
| 8 | + | ||||
| 9 | +<!-- END:nextjs-agent-rules --> | ||||
added
apps/web/UI-NOTES.md
+33 −0
@@ -0,0 +1,33 @@ | |||||
| 1 | +# UI notes — needs outside the UI scope (for the control-plane owner) | ||||
| 2 | + | ||||
| 3 | +## BLOCKER — `packages/core/src/papers.ts` related-papers query | ||||
| 4 | + | ||||
| 5 | +`getPaperBySlug` builds `sql\`${paperVersions.keywords} && ${row.v.keywords}\``. Drizzle expands the JS array as a tuple | ||||
| 6 | +(`&& ($4,$5,$6)`), so PostgreSQL rejects the query and **every paper page returns 500/404** (`/research/[slug]`and`/api/public/papers/[slug]`). Verified fix (one line, import `arrayOverlaps`from`drizzle-orm`): | ||||
| 7 | + | ||||
| 8 | +```ts | ||||
| 9 | +or(eq(papers.discipline, row.p.discipline), arrayOverlaps(paperVersions.keywords, row.v.keywords)); | ||||
| 10 | +``` | ||||
| 11 | + | ||||
| 12 | +The e2e suite was run with this fix applied temporarily (25/25 pass); the original file was restored afterwards and | ||||
| 13 | +`packages/core` rebuilt from it, so the paper page is broken again until the fix lands. | ||||
| 14 | + | ||||
| 15 | +## Tooling | ||||
| 16 | + | ||||
| 17 | +- `biome.json`: add `"css": { "parser": { "tailwindDirectives": true } }` so `apps/web/src/app/globals.css` (`@import "tailwindcss"`, `@theme`, `@apply`, `@custom-variant`) parses; today `pnpm lint` reports parse errors there. Also `biome migrate` (schema 2.4.0 vs CLI 2.6.4; `recommended` deprecated). | ||||
| 18 | +- pnpm 11 "deps status check" aborted `pnpm start` / `pnpm exec playwright` after dependencies changed (`ERR_PNPM_ABORTED_REMOVE_MODULES_DIR_NO_TTY` → it tried `pnpm install --production`). Workaround used: run binaries directly (`apps/web/node_modules/.bin/next`, `node_modules/.bin/playwright`). Consider `verify-deps-before-run=false` in `.npmrc` for non-interactive hosts (pm2). | ||||
| 19 | +- `next start` warns that `output: "standalone"` expects `node .next/standalone/server.js` (copy `.next/static` and `public` into the standalone dir on deploy). It still serves correctly; the pm2 manifest should pick one of the two. | ||||
| 20 | +- Rebuilding `.next` while a `next start` process is still serving leaves it with a stale client manifest ("Could not find the module …link.js in the React Client Manifest" → 500 on every page with `next/link`). Always stop the server before `next build` (or build to a fresh dir and swap). | ||||
| 21 | +- `packages/db/package.json`: added `pdf-lib` (used only by `src/seed.ts` to generate the staging PDF). | ||||
| 22 | + | ||||
| 23 | +## Server helpers / API observations (not changed) | ||||
| 24 | + | ||||
| 25 | +- `clientIp()` returns `null` when `TRUST_PROXY=0` and no proxy header is present (Next does not expose the socket address), so locally all visitors share the limiter keys `"unknown"`/`"public"`, and for `/api/internal/*` the CIDR allow-list is skipped (only the bearer token protects it). In production the worker reaches the control plane directly on the LAN (no proxy headers) → same situation. If a LAN-only guard is wanted, bind the internal API to a separate port/listener or set the socket address via a custom server. | ||||
| 26 | +- Internal worker routes answer **403** (`viaPublicProxy`) to Playwright's request context because it sends `x-forwarded-*`-free requests… actually they return 403 for any request whose headers include forwarded hints; the e2e asserts 401 _or_ 403 for those routes. | ||||
| 27 | +- The in-memory login limiter (10 logins / 15 min per username) is easy to hit while developing/testing; the e2e suite reuses one signed-in storage state to stay below it. A restart of the web process resets it. | ||||
| 28 | +- `listEvents()` returns `Date` objects; `redact()` turns a `Date` into `{}` (it walks it as a plain object). The run page converts dates before redacting; the `/api/admin/runs/[id]/events` route passes rows straight through `redact()` → `at` becomes `{}` in the JSON. Suggest `redact()` to pass `Date`/`Buffer` instances through unchanged, or serialise before redacting in that route. | ||||
| 29 | +- Public artifact route sets `accept-ranges: none`; the browser PDF viewer works but cannot seek by byte range on large PDFs. | ||||
| 30 | + | ||||
| 31 | +## Seed | ||||
| 32 | + | ||||
| 33 | +`pnpm db:seed` → `seedTopicsAndSeries()` always (17 disciplines + 10 subfields + series `as-wp`), `seedStagingPaper()` only with `STAGING_MODE=1` (one synthetic paper `AS-WP-2026-0000`, `papers.synthetic=true`, 6-page PDF written under `ARTIFACT_ROOT/public/AS-WP-2026-0000/v1/manuscript/main.pdf` with a `.sha256` sidecar). Production (`STAGING_MODE=0`) never lists, serves or indexes it. Remove it before launch with `delete from papers where public_id='AS-WP-2026-0000'` (cascades) + delete the public object. | ||||
added
apps/web/next-env.d.ts
+7 −0
@@ -0,0 +1,7 @@ | |||||
| 1 | +/// <reference types="next" /> | ||||
| 2 | +/// <reference types="next/image-types/global" /> | ||||
| 3 | +import "./.next/types/routes.d.ts"; | ||||
| 4 | +import "./.next/types/root-params.d.ts"; | ||||
| 5 | + | ||||
| 6 | +// NOTE: This file should not be edited | ||||
| 7 | +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. | ||||
added
apps/web/next.config.ts
+48 −0
@@ -0,0 +1,48 @@ | |||||
| 1 | +import type { NextConfig } from "next"; | ||||
| 2 | + | ||||
| 3 | +const isProd = process.env.NODE_ENV === "production"; | ||||
| 4 | +const staging = process.env.STAGING_MODE === "1"; | ||||
| 5 | + | ||||
| 6 | +const securityHeaders = [ | ||||
| 7 | + { key: "X-Content-Type-Options", value: "nosniff" }, | ||||
| 8 | + { key: "X-Frame-Options", value: "SAMEORIGIN" }, | ||||
| 9 | + { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" }, | ||||
| 10 | + { key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=(), browsing-topics=()" }, | ||||
| 11 | + { | ||||
| 12 | + key: "Content-Security-Policy", | ||||
| 13 | + value: [ | ||||
| 14 | + "default-src 'self'", | ||||
| 15 | + `script-src 'self'${isProd ? "" : " 'unsafe-eval'"} 'unsafe-inline'`, | ||||
| 16 | + "style-src 'self' 'unsafe-inline'", | ||||
| 17 | + "img-src 'self' data: blob:", | ||||
| 18 | + "font-src 'self' data:", | ||||
| 19 | + "connect-src 'self'", | ||||
| 20 | + "frame-src 'self' blob:", | ||||
| 21 | + "object-src 'self'", | ||||
| 22 | + "base-uri 'self'", | ||||
| 23 | + "form-action 'self'", | ||||
| 24 | + "frame-ancestors 'self'", | ||||
| 25 | + ...(isProd ? ["upgrade-insecure-requests"] : []), | ||||
| 26 | + ].join("; "), | ||||
| 27 | + }, | ||||
| 28 | + ...(isProd ? [{ key: "Strict-Transport-Security", value: "max-age=31536000; includeSubDomains" }] : []), | ||||
| 29 | + ...(staging ? [{ key: "X-Robots-Tag", value: "noindex, nofollow" }] : []), | ||||
| 30 | +]; | ||||
| 31 | + | ||||
| 32 | +const nextConfig: NextConfig = { | ||||
| 33 | + reactStrictMode: true, | ||||
| 34 | + poweredByHeader: false, | ||||
| 35 | + serverExternalPackages: ["pg", "pino", "@node-rs/argon2", "pdfjs-dist", "pdf-lib", "drizzle-orm"], | ||||
| 36 | + experimental: { serverActions: { bodySizeLimit: "2mb" } }, | ||||
| 37 | + async headers() { | ||||
| 38 | + return [ | ||||
| 39 | + { source: "/(.*)", headers: securityHeaders }, | ||||
| 40 | + { | ||||
| 41 | + source: "/papers/:path*", | ||||
| 42 | + headers: [{ key: "Cache-Control", value: "public, max-age=31536000, immutable" }], | ||||
| 43 | + }, | ||||
| 44 | + ]; | ||||
| 45 | + }, | ||||
| 46 | +}; | ||||
| 47 | + | ||||
| 48 | +export default nextConfig; | ||||
added
apps/web/package.json
+38 −0
@@ -0,0 +1,38 @@ | |||||
| 1 | +{ | ||||
| 2 | + "name": "@autoscholar/web", | ||||
| 3 | + "version": "0.1.0", | ||||
| 4 | + "private": true, | ||||
| 5 | + "type": "module", | ||||
| 6 | + "scripts": { | ||||
| 7 | + "dev": "next dev -p ${PORT:-8470}", | ||||
| 8 | + "build": "next build", | ||||
| 9 | + "start": "next start -H 0.0.0.0 -p ${PORT:-8470}", | ||||
| 10 | + "typecheck": "tsc --noEmit" | ||||
| 11 | + }, | ||||
| 12 | + "dependencies": { | ||||
| 13 | + "@autoscholar/auth": "workspace:*", | ||||
| 14 | + "@autoscholar/core": "workspace:*", | ||||
| 15 | + "@autoscholar/db": "workspace:*", | ||||
| 16 | + "@autoscholar/observability": "workspace:*", | ||||
| 17 | + "@autoscholar/publisher": "workspace:*", | ||||
| 18 | + "@autoscholar/qa": "workspace:*", | ||||
| 19 | + "@autoscholar/research-contract": "workspace:*", | ||||
| 20 | + "@autoscholar/schemas": "workspace:*", | ||||
| 21 | + "@autoscholar/storage": "workspace:*", | ||||
| 22 | + "drizzle-orm": "^0.45.0", | ||||
| 23 | + "marked": "^16.0.0", | ||||
| 24 | + "next": "^16.4.0", | ||||
| 25 | + "react": "^19.3.0", | ||||
| 26 | + "react-dom": "^19.3.0", | ||||
| 27 | + "sanitize-html": "^2.17.0", | ||||
| 28 | + "zod": "^4.1.0" | ||||
| 29 | + }, | ||||
| 30 | + "devDependencies": { | ||||
| 31 | + "@tailwindcss/postcss": "^4.1.0", | ||||
| 32 | + "@types/react": "^19.2.0", | ||||
| 33 | + "@types/react-dom": "^19.2.0", | ||||
| 34 | + "@types/sanitize-html": "^2.16.0", | ||||
| 35 | + "tailwindcss": "^4.1.0", | ||||
| 36 | + "typescript": "^5.9.0" | ||||
| 37 | + } | ||||
| 38 | +} | ||||
added
apps/web/postcss.config.mjs
+1 −0
@@ -0,0 +1 @@ | |||||
| 1 | +export default { plugins: { "@tailwindcss/postcss": {} } }; | ||||
added
apps/web/src/app/(public)/about/page.tsx
+55 −0
@@ -0,0 +1,55 @@ | |||||
| 1 | +import type { Metadata } from "next"; | ||||
| 2 | +import Link from "next/link"; | ||||
| 3 | + | ||||
| 4 | +export const metadata: Metadata = { title: "About", alternates: { canonical: "/about" } }; | ||||
| 5 | + | ||||
| 6 | +export default function AboutPage() { | ||||
| 7 | + return ( | ||||
| 8 | + <div className="prose-paper mx-auto max-w-3xl"> | ||||
| 9 | + <p className="eyebrow" style={{ fontFamily: "var(--font-sans)" }}> | ||||
| 10 | + About | ||||
| 11 | + </p> | ||||
| 12 | + <h1>AutoScholar.ai</h1> | ||||
| 13 | + <p> | ||||
| 14 | + AutoScholar is a research-producing startup and a public library of original working papers produced | ||||
| 15 | + by an autonomous, auditable AI research workflow. The product is not a chatbot, an essay generator or | ||||
| 16 | + a subscription for spun articles. It is a steadily growing body of research whose every release | ||||
| 17 | + carries its own evidence. | ||||
| 18 | + </p> | ||||
| 19 | + <h2>What we are</h2> | ||||
| 20 | + <p> | ||||
| 21 | + A one-person founder operation with an automated research pipeline. A single administrator writes | ||||
| 22 | + research instructions, launches runs on our compute infrastructure, and the platform plans, | ||||
| 23 | + investigates the literature, executes analyses or proofs, writes a LaTeX manuscript, subjects it to an | ||||
| 24 | + independent automated critical review, rebuilds and verifies the result, and publishes it only when | ||||
| 25 | + deterministic quality gates pass. | ||||
| 26 | + </p> | ||||
| 27 | + <h2>What we are not</h2> | ||||
| 28 | + <ul> | ||||
| 29 | + <li> | ||||
| 30 | + We are not a journal. AutoScholar working papers are preprints; they are{" "} | ||||
| 31 | + <strong>not peer reviewed</strong> by humans. | ||||
| 32 | + </li> | ||||
| 33 | + <li> | ||||
| 34 | + We do not have an editorial board, invented authors or academic affiliations. The byline of every | ||||
| 35 | + paper is “AutoScholar — AI-generated research”. | ||||
| 36 | + </li> | ||||
| 37 | + <li>We do not register DOIs. Papers are identified by stable AutoScholar working-paper numbers.</li> | ||||
| 38 | + <li>We do not report view counts, citation counts or ratings that we cannot verify.</li> | ||||
| 39 | + </ul> | ||||
| 40 | + <h2>Automation disclosure</h2> | ||||
| 41 | + <p> | ||||
| 42 | + Research planning, source retrieval, code, analysis, writing and automated review are performed by | ||||
| 43 | + large language models (Claude, via the Anthropic API) orchestrated by platform software with enforced | ||||
| 44 | + permissions and budgets. Humans set the research questions, configure policies, review gate outcomes, | ||||
| 45 | + and can correct or retract any paper. See the <Link href="/methodology">methodology</Link> and{" "} | ||||
| 46 | + <Link href="/transparency">transparency</Link> pages. | ||||
| 47 | + </p> | ||||
| 48 | + <h2>Infrastructure</h2> | ||||
| 49 | + <p> | ||||
| 50 | + Research is computed on MacLustr, an Apple Silicon research compute infrastructure. See{" "} | ||||
| 51 | + <Link href="/infrastructure">Powered by MacLustr</Link>. | ||||
| 52 | + </p> | ||||
| 53 | + </div> | ||||
| 54 | + ); | ||||
| 55 | +} | ||||
added
apps/web/src/app/(public)/infrastructure/page.tsx
+57 −0
@@ -0,0 +1,57 @@ | |||||
| 1 | +import type { Metadata } from "next"; | ||||
| 2 | + | ||||
| 3 | +export const metadata: Metadata = { | ||||
| 4 | + title: "Infrastructure — Powered by MacLustr", | ||||
| 5 | + alternates: { canonical: "/infrastructure" }, | ||||
| 6 | +}; | ||||
| 7 | + | ||||
| 8 | +export default function InfrastructurePage() { | ||||
| 9 | + return ( | ||||
| 10 | + <div className="mx-auto max-w-3xl"> | ||||
| 11 | + <p className="eyebrow">Powered by MacLustr</p> | ||||
| 12 | + <h1 className="serif mt-1 text-3xl font-semibold">Research computed on MacLustr</h1> | ||||
| 13 | + <p className="muted mt-3 leading-relaxed"> | ||||
| 14 | + AutoScholar is the research startup. MacLustr is its compute infrastructure: a private cluster of | ||||
| 15 | + Apple Silicon Macs operated as a research compute fabric, with a control plane that schedules work, | ||||
| 16 | + isolates each study and records what happened. | ||||
| 17 | + </p> | ||||
| 18 | + <div className="mt-8 grid gap-4 sm:grid-cols-2"> | ||||
| 19 | + {[ | ||||
| 20 | + [ | ||||
| 21 | + "Apple Silicon nodes", | ||||
| 22 | + "Research jobs run on Apple Silicon Mac nodes with unified memory, well suited to numerical work, local model inference and LaTeX builds.", | ||||
| 23 | + ], | ||||
| 24 | + [ | ||||
| 25 | + "Isolated per-run workspaces", | ||||
| 26 | + "Each run executes under a dedicated low-privilege service identity in a fresh workspace with OS-enforced sandboxing, restricted egress, and time, disk, memory and budget limits.", | ||||
| 27 | + ], | ||||
| 28 | + [ | ||||
| 29 | + "Durable scheduling", | ||||
| 30 | + "A durable queue with leases, heartbeats and fencing tokens means a run survives page reloads, network blips and worker restarts. Expired workers can never publish stale results.", | ||||
| 31 | + ], | ||||
| 32 | + [ | ||||
| 33 | + "Evidence first", | ||||
| 34 | + "Every artifact is hashed and versioned on write. Publication is a separate service the research agent cannot call.", | ||||
| 35 | + ], | ||||
| 36 | + ].map(([h, p]) => ( | ||||
| 37 | + <div key={h} className="surface p-5"> | ||||
| 38 | + <h2 className="serif text-lg font-semibold">{h}</h2> | ||||
| 39 | + <p className="muted mt-2 text-sm leading-relaxed">{p}</p> | ||||
| 40 | + </div> | ||||
| 41 | + ))} | ||||
| 42 | + </div> | ||||
| 43 | + <h2 className="serif mt-12 text-xl font-semibold">What we publish about the infrastructure</h2> | ||||
| 44 | + <p className="muted mt-2 text-sm leading-relaxed"> | ||||
| 45 | + We credit MacLustr on every paper and describe its design honestly. We do not publish node names, | ||||
| 46 | + addresses, capacities beyond what is measured, customer information or network topology. Claims about | ||||
| 47 | + hardware or performance are limited to what the current inventory and measurements support. | ||||
| 48 | + </p> | ||||
| 49 | + <h2 className="serif mt-10 text-xl font-semibold">Model execution</h2> | ||||
| 50 | + <p className="muted mt-2 text-sm leading-relaxed"> | ||||
| 51 | + Research runs use the Claude Agent SDK with API-key authentication. The platform records model | ||||
| 52 | + identifiers, token usage and cost estimates per stage; these appear in each paper’s provenance panel | ||||
| 53 | + where disclosure is appropriate. | ||||
| 54 | + </p> | ||||
| 55 | + </div> | ||||
| 56 | + ); | ||||
| 57 | +} | ||||
added
apps/web/src/app/(public)/layout.tsx
+18 −0
@@ -0,0 +1,18 @@ | |||||
| 1 | +import type { ReactNode } from "react"; | ||||
| 2 | +import { SiteFooter } from "@/components/SiteFooter"; | ||||
| 3 | +import { SiteHeader } from "@/components/SiteHeader"; | ||||
| 4 | + | ||||
| 5 | +export default function PublicLayout({ children }: { children: ReactNode }) { | ||||
| 6 | + return ( | ||||
| 7 | + <> | ||||
| 8 | + <a href="#main" className="skip-link"> | ||||
| 9 | + Skip to content | ||||
| 10 | + </a> | ||||
| 11 | + <SiteHeader /> | ||||
| 12 | + <main id="main" className="container-x py-10"> | ||||
| 13 | + {children} | ||||
| 14 | + </main> | ||||
| 15 | + <SiteFooter /> | ||||
| 16 | + </> | ||||
| 17 | + ); | ||||
| 18 | +} | ||||
added
apps/web/src/app/(public)/legal/privacy/page.tsx
+23 −0
@@ -0,0 +1,23 @@ | |||||
| 1 | +import type { Metadata } from "next"; | ||||
| 2 | +export const metadata: Metadata = { title: "Privacy", alternates: { canonical: "/legal/privacy" } }; | ||||
| 3 | +export default function Privacy() { | ||||
| 4 | + return ( | ||||
| 5 | + <div className="prose-paper mx-auto max-w-3xl"> | ||||
| 6 | + <h1>Privacy</h1> | ||||
| 7 | + <p> | ||||
| 8 | + Reading the library requires no account and no cookies for visitors. We keep standard server logs (IP | ||||
| 9 | + address, user agent, requested URL, timestamp) for security and operations for a limited period. We do | ||||
| 10 | + not use third-party analytics, advertising or tracking. | ||||
| 11 | + </p> | ||||
| 12 | + <p> | ||||
| 13 | + The private administration area sets a session cookie for its single administrator. Research | ||||
| 14 | + instructions, run logs and artifacts are stored privately until a paper is published; published | ||||
| 15 | + bundles are public and permanent, with private provenance elements redacted. | ||||
| 16 | + </p> | ||||
| 17 | + <p> | ||||
| 18 | + Questions about this policy can be sent to the operator through the contact details published on the | ||||
| 19 | + About page when available. | ||||
| 20 | + </p> | ||||
| 21 | + </div> | ||||
| 22 | + ); | ||||
| 23 | +} | ||||
added
apps/web/src/app/(public)/legal/research-disclaimer/page.tsx
+32 −0
@@ -0,0 +1,32 @@ | |||||
| 1 | +import type { Metadata } from "next"; | ||||
| 2 | +export const metadata: Metadata = { | ||||
| 3 | + title: "Research disclaimer", | ||||
| 4 | + alternates: { canonical: "/legal/research-disclaimer" }, | ||||
| 5 | +}; | ||||
| 6 | +export default function Disclaimer() { | ||||
| 7 | + return ( | ||||
| 8 | + <div className="prose-paper mx-auto max-w-3xl"> | ||||
| 9 | + <h1>Research and AI disclaimer</h1> | ||||
| 10 | + <p> | ||||
| 11 | + <strong> | ||||
| 12 | + Every AutoScholar paper is an AI-generated working paper. It has not been peer reviewed. | ||||
| 13 | + </strong>{" "} | ||||
| 14 | + It is a preprint produced by an autonomous workflow using large language models, verified by automated | ||||
| 15 | + checks that test structure, traceability and consistency, not truth. | ||||
| 16 | + </p> | ||||
| 17 | + <p> | ||||
| 18 | + Nothing on this site is medical, legal, financial or engineering advice. Results may contain errors, | ||||
| 19 | + including errors that the automated gates cannot detect. Readers who rely on a result should reproduce | ||||
| 20 | + it from the released code and data and consult qualified humans. | ||||
| 21 | + </p> | ||||
| 22 | + <p> | ||||
| 23 | + No paper carries a DOI, a human author, an institutional affiliation, an ethics approval or a claim of | ||||
| 24 | + peer review unless such a fact is explicitly and verifiably stated on the paper page. Any such | ||||
| 25 | + statement elsewhere is false. | ||||
| 26 | + </p> | ||||
| 27 | + <p> | ||||
| 28 | + MacLustr is credited as compute infrastructure. It is not a coauthor and does not endorse any result. | ||||
| 29 | + </p> | ||||
| 30 | + </div> | ||||
| 31 | + ); | ||||
| 32 | +} | ||||
added
apps/web/src/app/(public)/legal/terms/page.tsx
+27 −0
@@ -0,0 +1,27 @@ | |||||
| 1 | +import type { Metadata } from "next"; | ||||
| 2 | +export const metadata: Metadata = { title: "Terms of use", alternates: { canonical: "/legal/terms" } }; | ||||
| 3 | +export default function Terms() { | ||||
| 4 | + return ( | ||||
| 5 | + <div className="prose-paper mx-auto max-w-3xl"> | ||||
| 6 | + <h1>Terms of use</h1> | ||||
| 7 | + <p> | ||||
| 8 | + AutoScholar working papers and their accompanying artifacts are provided as is, without warranty of | ||||
| 9 | + any kind. Licences for paper text, code and data are stated separately on each paper page when they | ||||
| 10 | + were part of the research; where no licence is stated, all rights are reserved. | ||||
| 11 | + </p> | ||||
| 12 | + <p> | ||||
| 13 | + You may read, download and cite papers. Citation guidance is provided on each paper page. You may not | ||||
| 14 | + misrepresent an AutoScholar working paper as a peer-reviewed publication or as the work of a human | ||||
| 15 | + author. | ||||
| 16 | + </p> | ||||
| 17 | + <p> | ||||
| 18 | + The site, its API and its feeds may be rate-limited. Automated access must respect robots directives. | ||||
| 19 | + We may correct or retract papers at any time; corrections and retractions are public and permanent. | ||||
| 20 | + </p> | ||||
| 21 | + <p> | ||||
| 22 | + These terms do not create any legal guarantee of accuracy, fitness for a purpose or regulatory | ||||
| 23 | + compliance. | ||||
| 24 | + </p> | ||||
| 25 | + </div> | ||||
| 26 | + ); | ||||
| 27 | +} | ||||
added
apps/web/src/app/(public)/methodology/page.tsx
+72 −0
@@ -0,0 +1,72 @@ | |||||
| 1 | +import { STAGES } from "@autoscholar/schemas"; | ||||
| 2 | +import type { Metadata } from "next"; | ||||
| 3 | + | ||||
| 4 | +export const metadata: Metadata = { title: "Methodology", alternates: { canonical: "/methodology" } }; | ||||
| 5 | + | ||||
| 6 | +const DETAILS: Record<string, string> = { | ||||
| 7 | + preflight: | ||||
| 8 | + "The frozen research instruction is parsed; scientific scope, feasibility, restricted or regulated activities, missing inputs and budgets are assessed. Unsafe or impossible studies are rejected with reasons.", | ||||
| 9 | + framing: | ||||
| 10 | + "A precise question, hypotheses or propositions, the intended contribution and a discipline-appropriate methodology are recorded, with evaluation criteria fixed before any test is run and honest null-result pathways identified.", | ||||
| 11 | + literature: | ||||
| 12 | + "Real scholarly registries (Crossref, OpenAlex, arXiv, PubMed, Semantic Scholar as applicable) are searched. Each reference is verified by identifier before it can be cited; a claim-to-source map is built. No reference is cited merely because a model believes it exists.", | ||||
| 13 | + design: | ||||
| 14 | + "Depending on the mode: genuine, legally usable data with documented provenance and licence; formal definitions and planned derivations; benchmark environments with baselines, seeds and leakage controls; or a search protocol with inclusion criteria. Synthetic simulations are labelled as synthetic.", | ||||
| 15 | + execute: | ||||
| 16 | + "Real code, experiments, simulations, formal checks or substantive qualitative work are executed in an isolated workspace. Environment, versions, seeds, commands and outputs are logged, including failures and negative findings.", | ||||
| 17 | + robustness: | ||||
| 18 | + "Sensitivity analyses, alternative specifications, stronger baselines, stress tests, proof checks or counterexample searches are run, and results that weaken the contribution are reported.", | ||||
| 19 | + manuscript: | ||||
| 20 | + "A LaTeX manuscript is generated from the recorded results with a real BibTeX database; every reported number must derive from stored artifacts.", | ||||
| 21 | + review: | ||||
| 22 | + "A separate review context that did not author the manuscript critiques novelty, validity, evidence, mathematics, statistics, references, ethics and writing. A response-to-review document records revisions. This is automated critique, not human peer review.", | ||||
| 23 | + reproduction: | ||||
| 24 | + "Scripts are re-executed and the PDF is rebuilt in a clean environment. Tables, figures, arithmetic, citation identifiers, fonts and build health are checked; representative pages are inspected.", | ||||
| 25 | + quality: | ||||
| 26 | + "Deterministic gates run on the control plane, independent of the authoring model. A hard failure blocks publication regardless of any review score.", | ||||
| 27 | + publication: | ||||
| 28 | + "A permanent paper number is allocated, artifacts are promoted with their digests, and the paper, feed and sitemap are updated transactionally. Automatic release requires every hard gate to pass; sensitive modes require human approval.", | ||||
| 29 | +}; | ||||
| 30 | + | ||||
| 31 | +export default function MethodologyPage() { | ||||
| 32 | + return ( | ||||
| 33 | + <div className="mx-auto max-w-3xl"> | ||||
| 34 | + <p className="eyebrow">Methodology</p> | ||||
| 35 | + <h1 className="serif mt-1 text-3xl font-semibold">How a working paper is produced</h1> | ||||
| 36 | + <p className="muted mt-3"> | ||||
| 37 | + Production is a durable state machine of stages with evidence requirements, not a single enormous | ||||
| 38 | + prompt and not a cast of fictional named agents. The platform policy controls permissions, sandboxes, | ||||
| 39 | + budgets and publication; the uploaded research instruction controls only the academic task. | ||||
| 40 | + </p> | ||||
| 41 | + <ol className="mt-8 space-y-6"> | ||||
| 42 | + {STAGES.map((s) => ( | ||||
| 43 | + <li key={s.key} className="grid gap-2 border-t pt-4 hairline sm:grid-cols-[4rem_1fr]"> | ||||
| 44 | + <span className="font-mono text-sm subtle">Stage {s.index}</span> | ||||
| 45 | + <div> | ||||
| 46 | + <h2 className="serif text-lg font-semibold">{s.name}</h2> | ||||
| 47 | + <p className="muted mt-1 text-sm leading-relaxed">{DETAILS[s.key]}</p> | ||||
| 48 | + </div> | ||||
| 49 | + </li> | ||||
| 50 | + ))} | ||||
| 51 | + </ol> | ||||
| 52 | + <h2 className="serif mt-12 text-xl font-semibold">Research modes</h2> | ||||
| 53 | + <p className="muted mt-2 text-sm"> | ||||
| 54 | + Empirical, computational, theoretical, review, experimental and humanities modes each carry their own | ||||
| 55 | + required artifacts and checks. Mixed-mode studies are allowed; a template never forces an experiment | ||||
| 56 | + or a dataset that does not belong. Human participants, clinical interventions or biosafety-relevant | ||||
| 57 | + work are out of scope unless documented approvals exist; otherwise work is restricted to lawful | ||||
| 58 | + literature, public data, mathematics and simulations. | ||||
| 59 | + </p> | ||||
| 60 | + <h2 className="serif mt-10 text-xl font-semibold">Research standards</h2> | ||||
| 61 | + <ul className="muted mt-2 list-disc space-y-1 pl-5 text-sm"> | ||||
| 62 | + <li>Length is not quality. A concise correct result outranks a long decorated one.</li> | ||||
| 63 | + <li> | ||||
| 64 | + No invented references, datasets, results, proofs, significance, approvals, affiliations, identities | ||||
| 65 | + or reproducibility claims. | ||||
| 66 | + </li> | ||||
| 67 | + <li>No silent re-specification until a desired p-value appears; negative findings are preserved.</li> | ||||
| 68 | + <li>All external content is untrusted data; it cannot alter platform rules.</li> | ||||
| 69 | + </ul> | ||||
| 70 | + </div> | ||||
| 71 | + ); | ||||
| 72 | +} | ||||
added
apps/web/src/app/(public)/page.tsx
+140 −0
@@ -0,0 +1,140 @@ | |||||
| 1 | +import { disciplineCounts, listTopics, searchPapers } from "@autoscholar/core"; | ||||
| 2 | +import { DISCIPLINE_LABELS, PublicSearchQuery } from "@autoscholar/schemas"; | ||||
| 3 | +import Link from "next/link"; | ||||
| 4 | +import { Disclosure } from "@/components/Disclosure"; | ||||
| 5 | +import { EmptyState } from "@/components/EmptyState"; | ||||
| 6 | +import { PaperCard } from "@/components/PaperCard"; | ||||
| 7 | +import { t } from "@/lib/i18n"; | ||||
| 8 | +import { db } from "@/lib/server/db"; | ||||
| 9 | + | ||||
| 10 | +export const dynamic = "force-dynamic"; | ||||
| 11 | + | ||||
| 12 | +export default async function Home() { | ||||
| 13 | + const d = db(); | ||||
| 14 | + const latest = await searchPapers(d, PublicSearchQuery.parse({ pageSize: "7" })); | ||||
| 15 | + const [featured, ...recent] = latest.items; | ||||
| 16 | + const counts = await disciplineCounts(d); | ||||
| 17 | + const topics = await listTopics(d); | ||||
| 18 | + return ( | ||||
| 19 | + <div className="space-y-20"> | ||||
| 20 | + <section className="grid gap-10 md:grid-cols-12 md:items-end"> | ||||
| 21 | + <div className="md:col-span-8"> | ||||
| 22 | + <p className="eyebrow">Autonomous Research Foundry</p> | ||||
| 23 | + <h1 className="serif mt-3 text-4xl font-semibold leading-[1.1] tracking-tight sm:text-5xl lg:text-6xl"> | ||||
| 24 | + From research instructions to verifiable working papers. | ||||
| 25 | + </h1> | ||||
| 26 | + <p className="muted mt-6 max-w-2xl text-lg leading-relaxed"> | ||||
| 27 | + AutoScholar is a research-producing startup and a public library of original, AI-executed working | ||||
| 28 | + papers across disciplines. Each paper is produced by an auditable autonomous workflow, checked by | ||||
| 29 | + deterministic quality gates, and released with its evidence: sources, code, data, build logs and | ||||
| 30 | + provenance. | ||||
| 31 | + </p> | ||||
| 32 | + <div className="mt-8 flex flex-wrap gap-3"> | ||||
| 33 | + <Link href="/research" className="btn btn-primary"> | ||||
| 34 | + Browse the library | ||||
| 35 | + </Link> | ||||
| 36 | + <Link href="/methodology" className="btn"> | ||||
| 37 | + How papers are produced | ||||
| 38 | + </Link> | ||||
| 39 | + </div> | ||||
| 40 | + </div> | ||||
| 41 | + <aside className="surface p-5 text-sm md:col-span-4"> | ||||
| 42 | + <p className="eyebrow">Powered by MacLustr</p> | ||||
| 43 | + <p className="muted mt-2"> | ||||
| 44 | + {t("footer.credit")} Every run executes in an isolated workspace with enforced budgets, and | ||||
| 45 | + nothing is published unless every hard gate passes. | ||||
| 46 | + </p> | ||||
| 47 | + <Link href="/infrastructure" className="mt-3 inline-block"> | ||||
| 48 | + About the infrastructure → | ||||
| 49 | + </Link> | ||||
| 50 | + </aside> | ||||
| 51 | + </section> | ||||
| 52 | + | ||||
| 53 | + <section aria-labelledby="latest"> | ||||
| 54 | + <div className="flex items-end justify-between gap-4"> | ||||
| 55 | + <div> | ||||
| 56 | + <p className="eyebrow">Library</p> | ||||
| 57 | + <h2 id="latest" className="serif mt-1 text-2xl font-semibold"> | ||||
| 58 | + New releases | ||||
| 59 | + </h2> | ||||
| 60 | + </div> | ||||
| 61 | + <Link href="/research" className="text-sm"> | ||||
| 62 | + All working papers → | ||||
| 63 | + </Link> | ||||
| 64 | + </div> | ||||
| 65 | + <div className="mt-6"> | ||||
| 66 | + {!featured ? ( | ||||
| 67 | + <EmptyState title="No working papers have been released yet.">{t("library.empty")}</EmptyState> | ||||
| 68 | + ) : ( | ||||
| 69 | + <div className="grid gap-4 md:grid-cols-12"> | ||||
| 70 | + <div className="md:col-span-7"> | ||||
| 71 | + <PaperCard p={featured} featured /> | ||||
| 72 | + </div> | ||||
| 73 | + <div className="grid gap-4 md:col-span-5"> | ||||
| 74 | + {recent.slice(0, 3).map((p) => ( | ||||
| 75 | + <PaperCard key={p.publicId} p={p} /> | ||||
| 76 | + ))} | ||||
| 77 | + </div> | ||||
| 78 | + {recent.slice(3).map((p) => ( | ||||
| 79 | + <div key={p.publicId} className="md:col-span-4"> | ||||
| 80 | + <PaperCard p={p} /> | ||||
| 81 | + </div> | ||||
| 82 | + ))} | ||||
| 83 | + </div> | ||||
| 84 | + )} | ||||
| 85 | + </div> | ||||
| 86 | + <div className="mt-6"> | ||||
| 87 | + <Disclosure /> | ||||
| 88 | + </div> | ||||
| 89 | + </section> | ||||
| 90 | + | ||||
| 91 | + <section aria-labelledby="subjects"> | ||||
| 92 | + <p className="eyebrow">Subject explorer</p> | ||||
| 93 | + <h2 id="subjects" className="serif mt-1 text-2xl font-semibold"> | ||||
| 94 | + Disciplines | ||||
| 95 | + </h2> | ||||
| 96 | + <ul className="mt-6 grid gap-2 sm:grid-cols-2 lg:grid-cols-3"> | ||||
| 97 | + {Object.entries(DISCIPLINE_LABELS).map(([slug, name]) => { | ||||
| 98 | + const n = counts.find((c) => c.discipline === slug)?.n ?? 0; | ||||
| 99 | + const topic = topics.find((x) => x.slug === slug); | ||||
| 100 | + return ( | ||||
| 101 | + <li key={slug}> | ||||
| 102 | + <Link | ||||
| 103 | + href={topic ? `/topics/${slug}` : `/research?discipline=${slug}`} | ||||
| 104 | + className="surface flex items-center justify-between px-4 py-3 text-sm no-underline hover:bg-[var(--bg-muted)]" | ||||
| 105 | + > | ||||
| 106 | + <span>{name}</span> | ||||
| 107 | + <span className="subtle font-mono text-xs"> | ||||
| 108 | + {n} {n === 1 ? "paper" : "papers"} | ||||
| 109 | + </span> | ||||
| 110 | + </Link> | ||||
| 111 | + </li> | ||||
| 112 | + ); | ||||
| 113 | + })} | ||||
| 114 | + </ul> | ||||
| 115 | + </section> | ||||
| 116 | + | ||||
| 117 | + <section className="grid gap-6 md:grid-cols-3"> | ||||
| 118 | + {[ | ||||
| 119 | + [ | ||||
| 120 | + "Scientific integrity before throughput", | ||||
| 121 | + "No paper is published automatically unless its central claims trace to verified sources or executed artifacts, its citations resolve, and its PDF builds from the released LaTeX source.", | ||||
| 122 | + ], | ||||
| 123 | + [ | ||||
| 124 | + "Evidence travels with the paper", | ||||
| 125 | + "Each release ships the manuscript source, verified bibliography, code and data when they were part of the study, a reproducibility note, and a redacted provenance record.", | ||||
| 126 | + ], | ||||
| 127 | + [ | ||||
| 128 | + "Honest about what this is", | ||||
| 129 | + "These are AI-generated working papers, not peer-reviewed articles. Automated review is critique, not validation. Corrections and retractions are public and permanent.", | ||||
| 130 | + ], | ||||
| 131 | + ].map(([h, p]) => ( | ||||
| 132 | + <div key={h} className="border-t pt-4 hairline"> | ||||
| 133 | + <h3 className="serif text-lg font-semibold">{h}</h3> | ||||
| 134 | + <p className="muted mt-2 text-sm leading-relaxed">{p}</p> | ||||
| 135 | + </div> | ||||
| 136 | + ))} | ||||
| 137 | + </section> | ||||
| 138 | + </div> | ||||
| 139 | + ); | ||||
| 140 | +} | ||||
added
apps/web/src/app/(public)/research/[slug]/cite.bib/route.ts
+29 −0
@@ -0,0 +1,29 @@ | |||||
| 1 | +import { bibtexFor, getPaperBySlug } from "@autoscholar/core"; | ||||
| 2 | +import { handler } from "@/lib/server/api"; | ||||
| 3 | +import { db } from "@/lib/server/db"; | ||||
| 4 | + | ||||
| 5 | +export const runtime = "nodejs"; | ||||
| 6 | + | ||||
| 7 | +export const GET = handler(async (_req, { params }) => { | ||||
| 8 | + const { slug } = (await params) as { slug: string }; | ||||
| 9 | + const p = await getPaperBySlug(db(), slug); | ||||
| 10 | + const base = process.env.APP_BASE_URL ?? "http://localhost:8470"; | ||||
| 11 | + const pub = new Date(p.version.publishedAt); | ||||
| 12 | + const text = bibtexFor({ | ||||
| 13 | + publicId: p.paper.publicId, | ||||
| 14 | + title: p.version.title, | ||||
| 15 | + year: pub.getUTCFullYear(), | ||||
| 16 | + month: pub.getUTCMonth() + 1, | ||||
| 17 | + versionNo: p.version.versionNo, | ||||
| 18 | + url: `${base}/research/${p.paper.slug}`, | ||||
| 19 | + keywords: p.version.keywords, | ||||
| 20 | + abstract: p.version.abstract, | ||||
| 21 | + }); | ||||
| 22 | + return new Response(text, { | ||||
| 23 | + headers: { | ||||
| 24 | + "content-type": "application/x-bibtex; charset=utf-8", | ||||
| 25 | + "content-disposition": `attachment; filename="${p.paper.publicId}.bib"`, | ||||
| 26 | + "cache-control": "public, max-age=3600", | ||||
| 27 | + }, | ||||
| 28 | + }); | ||||
| 29 | +}); | ||||
added
apps/web/src/app/(public)/research/[slug]/cite.ris/route.ts
+29 −0
@@ -0,0 +1,29 @@ | |||||
| 1 | +import { getPaperBySlug, risFor } from "@autoscholar/core"; | ||||
| 2 | +import { handler } from "@/lib/server/api"; | ||||
| 3 | +import { db } from "@/lib/server/db"; | ||||
| 4 | + | ||||
| 5 | +export const runtime = "nodejs"; | ||||
| 6 | + | ||||
| 7 | +export const GET = handler(async (_req, { params }) => { | ||||
| 8 | + const { slug } = (await params) as { slug: string }; | ||||
| 9 | + const p = await getPaperBySlug(db(), slug); | ||||
| 10 | + const base = process.env.APP_BASE_URL ?? "http://localhost:8470"; | ||||
| 11 | + const pub = new Date(p.version.publishedAt); | ||||
| 12 | + const text = risFor({ | ||||
| 13 | + publicId: p.paper.publicId, | ||||
| 14 | + title: p.version.title, | ||||
| 15 | + year: pub.getUTCFullYear(), | ||||
| 16 | + month: pub.getUTCMonth() + 1, | ||||
| 17 | + versionNo: p.version.versionNo, | ||||
| 18 | + url: `${base}/research/${p.paper.slug}`, | ||||
| 19 | + keywords: p.version.keywords, | ||||
| 20 | + abstract: p.version.abstract, | ||||
| 21 | + }); | ||||
| 22 | + return new Response(text, { | ||||
| 23 | + headers: { | ||||
| 24 | + "content-type": "application/x-research-info-systems; charset=utf-8", | ||||
| 25 | + "content-disposition": `attachment; filename="${p.paper.publicId}.ris"`, | ||||
| 26 | + "cache-control": "public, max-age=3600", | ||||
| 27 | + }, | ||||
| 28 | + }); | ||||
| 29 | +}); | ||||
added
apps/web/src/app/(public)/research/[slug]/cite.txt/route.ts
+29 −0
@@ -0,0 +1,29 @@ | |||||
| 1 | +import { getPaperBySlug, plainCitationFor } from "@autoscholar/core"; | ||||
| 2 | +import { handler } from "@/lib/server/api"; | ||||
| 3 | +import { db } from "@/lib/server/db"; | ||||
| 4 | + | ||||
| 5 | +export const runtime = "nodejs"; | ||||
| 6 | + | ||||
| 7 | +export const GET = handler(async (_req, { params }) => { | ||||
| 8 | + const { slug } = (await params) as { slug: string }; | ||||
| 9 | + const p = await getPaperBySlug(db(), slug); | ||||
| 10 | + const base = process.env.APP_BASE_URL ?? "http://localhost:8470"; | ||||
| 11 | + const pub = new Date(p.version.publishedAt); | ||||
| 12 | + const text = plainCitationFor({ | ||||
| 13 | + publicId: p.paper.publicId, | ||||
| 14 | + title: p.version.title, | ||||
| 15 | + year: pub.getUTCFullYear(), | ||||
| 16 | + month: pub.getUTCMonth() + 1, | ||||
| 17 | + versionNo: p.version.versionNo, | ||||
| 18 | + url: `${base}/research/${p.paper.slug}`, | ||||
| 19 | + keywords: p.version.keywords, | ||||
| 20 | + abstract: p.version.abstract, | ||||
| 21 | + }); | ||||
| 22 | + return new Response(text, { | ||||
| 23 | + headers: { | ||||
| 24 | + "content-type": "text/plain; charset=utf-8", | ||||
| 25 | + "content-disposition": `attachment; filename="${p.paper.publicId}.txt"`, | ||||
| 26 | + "cache-control": "public, max-age=3600", | ||||
| 27 | + }, | ||||
| 28 | + }); | ||||
| 29 | +}); | ||||
added
apps/web/src/app/(public)/research/[slug]/page.tsx
+665 −0
@@ -0,0 +1,665 @@ | |||||
| 1 | +import { bibtexFor, getPaperBySlug, plainCitationFor, risFor } from "@autoscholar/core"; | ||||
| 2 | +import { parseBib } from "@autoscholar/qa"; | ||||
| 3 | +import { publicArtifactKey } from "@autoscholar/storage"; | ||||
| 4 | +import type { Metadata } from "next"; | ||||
| 5 | +import Link from "next/link"; | ||||
| 6 | +import { notFound } from "next/navigation"; | ||||
| 7 | +import { CopyButton } from "@/components/CopyButton"; | ||||
| 8 | +import { Disclosure } from "@/components/Disclosure"; | ||||
| 9 | +import { PaperCard } from "@/components/PaperCard"; | ||||
| 10 | +import { PdfViewer } from "@/components/PdfViewer"; | ||||
| 11 | +import { StatusBadge } from "@/components/StatusBadge"; | ||||
| 12 | +import { Tabs } from "@/components/Tabs"; | ||||
| 13 | +import { disciplineLabel, fmtBytes, fmtDate, modeLabel, shortHash } from "@/lib/format"; | ||||
| 14 | +import { t } from "@/lib/i18n"; | ||||
| 15 | +import { db, storage } from "@/lib/server/db"; | ||||
| 16 | + | ||||
| 17 | +export const dynamic = "force-dynamic"; | ||||
| 18 | + | ||||
| 19 | +async function load(slug: string) { | ||||
| 20 | + try { | ||||
| 21 | + return await getPaperBySlug(db(), slug); | ||||
| 22 | + } catch { | ||||
| 23 | + return null; | ||||
| 24 | + } | ||||
| 25 | +} | ||||
| 26 | + | ||||
| 27 | +export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> { | ||||
| 28 | + const { slug } = await params; | ||||
| 29 | + const p = await load(slug); | ||||
| 30 | + if (!p) return { title: "Paper not found" }; | ||||
| 31 | + const base = process.env.APP_BASE_URL ?? "http://localhost:8470"; | ||||
| 32 | + return { | ||||
| 33 | + title: p.version.title, | ||||
| 34 | + description: p.version.abstract.slice(0, 300), | ||||
| 35 | + alternates: { canonical: `/research/${p.paper.slug}` }, | ||||
| 36 | + robots: p.paper.synthetic ? { index: false, follow: false } : undefined, | ||||
| 37 | + openGraph: { | ||||
| 38 | + title: p.version.title, | ||||
| 39 | + description: p.version.abstract.slice(0, 300), | ||||
| 40 | + type: "article", | ||||
| 41 | + url: `${base}/research/${p.paper.slug}`, | ||||
| 42 | + publishedTime: p.version.publishedAt.toISOString(), | ||||
| 43 | + }, | ||||
| 44 | + other: { | ||||
| 45 | + citation_title: p.version.title, | ||||
| 46 | + citation_publication_date: p.version.publishedAt.toISOString().slice(0, 10), | ||||
| 47 | + citation_technical_report_number: p.paper.publicId, | ||||
| 48 | + citation_technical_report_institution: "AutoScholar.ai", | ||||
| 49 | + citation_pdf_url: `${base}${p.version.pdfPublicPath}`, | ||||
| 50 | + }, | ||||
| 51 | + }; | ||||
| 52 | +} | ||||
| 53 | + | ||||
| 54 | +type ArtifactEntry = { | ||||
| 55 | + path: string; | ||||
| 56 | + kind: string; | ||||
| 57 | + sha256: string; | ||||
| 58 | + bytes: number; | ||||
| 59 | + contentType: string; | ||||
| 60 | + publicPath: string; | ||||
| 61 | +}; | ||||
| 62 | +function Dl({ a }: { a: ArtifactEntry }) { | ||||
| 63 | + return ( | ||||
| 64 | + <li className="flex items-center justify-between gap-3 py-1.5 text-sm"> | ||||
| 65 | + <a href={`${a.publicPath}?download=1`} className="font-mono text-xs break-all"> | ||||
| 66 | + {a.path} | ||||
| 67 | + </a> | ||||
| 68 | + <span className="subtle whitespace-nowrap text-xs"> | ||||
| 69 | + {fmtBytes(a.bytes)} · {shortHash(a.sha256, 8)} | ||||
| 70 | + </span> | ||||
| 71 | + </li> | ||||
| 72 | + ); | ||||
| 73 | +} | ||||
| 74 | + | ||||
| 75 | +export default async function PaperPage({ params }: { params: Promise<{ slug: string }> }) { | ||||
| 76 | + const { slug } = await params; | ||||
| 77 | + const p = await load(slug); | ||||
| 78 | + if (!p) notFound(); | ||||
| 79 | + const base = process.env.APP_BASE_URL ?? "http://localhost:8470"; | ||||
| 80 | + const v = p.version; | ||||
| 81 | + const meta = v.metadata as Record<string, unknown>; | ||||
| 82 | + const withdrawn = meta.artifactsWithdrawn === true; | ||||
| 83 | + const set = withdrawn ? [] : v.artifactSet; | ||||
| 84 | + const url = `${base}/research/${p.paper.slug}`; | ||||
| 85 | + const pub = new Date(v.publishedAt); | ||||
| 86 | + const cite = { | ||||
| 87 | + publicId: p.paper.publicId, | ||||
| 88 | + title: v.title, | ||||
| 89 | + year: pub.getUTCFullYear(), | ||||
| 90 | + month: pub.getUTCMonth() + 1, | ||||
| 91 | + versionNo: v.versionNo, | ||||
| 92 | + url, | ||||
| 93 | + keywords: v.keywords, | ||||
| 94 | + abstract: v.abstract, | ||||
| 95 | + }; | ||||
| 96 | + const figures = set.filter((a) => a.kind === "figure" && /\.(png|svg)$/i.test(a.path)); | ||||
| 97 | + const pdfFigures = set.filter((a) => a.kind === "figure" && /\.pdf$/i.test(a.path)); | ||||
| 98 | + const tex = set.filter((a) => a.kind === "manuscript" && /\.(tex|bib|cls|sty)$/i.test(a.path)); | ||||
| 99 | + const code = set.filter((a) => a.kind === "code"); | ||||
| 100 | + const data = set.filter((a) => a.kind === "data"); | ||||
| 101 | + const results = set.filter((a) => a.kind === "result" || a.kind === "table"); | ||||
| 102 | + const prov = (v.provenance ?? {}) as Record<string, unknown>; | ||||
| 103 | + const qa = (v.qaSummary ?? {}) as { | ||||
| 104 | + gates?: Array<{ gate: string; status: string; severity: string; message: string }>; | ||||
| 105 | + warnings?: string[]; | ||||
| 106 | + policyVersion?: string; | ||||
| 107 | + evaluatedAt?: string; | ||||
| 108 | + }; | ||||
| 109 | + let bib: ReturnType<typeof parseBib> = []; | ||||
| 110 | + const bibEntry = set.find((a) => a.path === "sources/references.bib"); | ||||
| 111 | + if (bibEntry) { | ||||
| 112 | + try { | ||||
| 113 | + bib = parseBib( | ||||
| 114 | + (await storage().get(publicArtifactKey(p.paper.publicId, v.versionNo, bibEntry.path))).toString( | ||||
| 115 | + "utf8", | ||||
| 116 | + ), | ||||
| 117 | + ); | ||||
| 118 | + } catch { | ||||
| 119 | + bib = []; | ||||
| 120 | + } | ||||
| 121 | + } | ||||
| 122 | + const jsonLd = { | ||||
| 123 | + "@context": "https://schema.org", | ||||
| 124 | + "@type": "ScholarlyArticle", | ||||
| 125 | + headline: v.title, | ||||
| 126 | + name: v.title, | ||||
| 127 | + abstract: v.abstract, | ||||
| 128 | + keywords: v.keywords.join(", "), | ||||
| 129 | + datePublished: v.publishedAt.toISOString(), | ||||
| 130 | + dateModified: p.paper.updatedAt.toISOString(), | ||||
| 131 | + author: { "@type": "Organization", name: "AutoScholar (AI-generated research)", url: base }, | ||||
| 132 | + publisher: { "@type": "Organization", name: "AutoScholar.ai", url: base }, | ||||
| 133 | + identifier: p.paper.publicId, | ||||
| 134 | + url, | ||||
| 135 | + isAccessibleForFree: true, | ||||
| 136 | + ...(set.length | ||||
| 137 | + ? { | ||||
| 138 | + encoding: { | ||||
| 139 | + "@type": "MediaObject", | ||||
| 140 | + contentUrl: `${base}${v.pdfPublicPath}`, | ||||
| 141 | + encodingFormat: "application/pdf", | ||||
| 142 | + }, | ||||
| 143 | + } | ||||
| 144 | + : {}), | ||||
| 145 | + about: disciplineLabel(p.paper.discipline), | ||||
| 146 | + version: v.versionNo, | ||||
| 147 | + ...(p.retraction ? { creativeWorkStatus: "Retracted" } : {}), | ||||
| 148 | + }; | ||||
| 149 | + return ( | ||||
| 150 | + <article className="space-y-10"> | ||||
| 151 | + {/* biome-ignore lint/security/noDangerouslySetInnerHtml: JSON-LD built from server data */} | ||||
| 152 | + <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} /> | ||||
| 153 | + {p.retraction && ( | ||||
| 154 | + <div | ||||
| 155 | + role="alert" | ||||
| 156 | + className="rounded-md border p-4" | ||||
| 157 | + style={{ | ||||
| 158 | + borderColor: "var(--color-danger-500)", | ||||
| 159 | + background: "color-mix(in oklab, var(--color-danger-500) 8%, transparent)", | ||||
| 160 | + }} | ||||
| 161 | + > | ||||
| 162 | + <p className="font-semibold"> | ||||
| 163 | + This working paper was retracted on {fmtDate(p.retraction.retractedAt)}. | ||||
| 164 | + </p> | ||||
| 165 | + <p className="mt-1 text-sm">{p.retraction.notice}</p> | ||||
| 166 | + {withdrawn && ( | ||||
| 167 | + <p className="muted mt-1 text-sm"> | ||||
| 168 | + Its artifacts have been withdrawn from delivery. This page remains as a permanent record. | ||||
| 169 | + </p> | ||||
| 170 | + )} | ||||
| 171 | + </div> | ||||
| 172 | + )} | ||||
| 173 | + <header className="grid gap-6 md:grid-cols-12"> | ||||
| 174 | + <div className="md:col-span-8"> | ||||
| 175 | + <div className="flex flex-wrap items-center gap-2 text-xs"> | ||||
| 176 | + <span className="font-mono">{p.paper.publicId}</span> | ||||
| 177 | + <span className="subtle">·</span> | ||||
| 178 | + <span>Version {v.versionNo}</span> | ||||
| 179 | + <StatusBadge status={p.paper.status} /> | ||||
| 180 | + {p.paper.synthetic && <span className="badge">staging sample — not a real paper</span>} | ||||
| 181 | + </div> | ||||
| 182 | + <h1 className="serif mt-3 text-3xl font-semibold leading-tight sm:text-4xl">{v.title}</h1> | ||||
| 183 | + <p className="mt-4 text-sm"> | ||||
| 184 | + <span className="font-medium">{t("byline")}</span> | ||||
| 185 | + <span className="muted"> · Research computed on MacLustr</span> | ||||
| 186 | + </p> | ||||
| 187 | + <dl className="muted mt-4 grid grid-cols-2 gap-x-6 gap-y-1 text-sm sm:grid-cols-4"> | ||||
| 188 | + <div> | ||||
| 189 | + <dt className="label">Released</dt> | ||||
| 190 | + <dd>{fmtDate(p.paper.firstPublishedAt)}</dd> | ||||
| 191 | + </div> | ||||
| 192 | + <div> | ||||
| 193 | + <dt className="label">Revised</dt> | ||||
| 194 | + <dd>{fmtDate(v.publishedAt)}</dd> | ||||
| 195 | + </div> | ||||
| 196 | + <div> | ||||
| 197 | + <dt className="label">Subject</dt> | ||||
| 198 | + <dd> | ||||
| 199 | + <Link href={`/research?discipline=${p.paper.discipline}`}> | ||||
| 200 | + {disciplineLabel(p.paper.discipline)} | ||||
| 201 | + </Link> | ||||
| 202 | + </dd> | ||||
| 203 | + </div> | ||||
| 204 | + <div> | ||||
| 205 | + <dt className="label">Methodology</dt> | ||||
| 206 | + <dd>{modeLabel(p.paper.researchMode)}</dd> | ||||
| 207 | + </div> | ||||
| 208 | + </dl> | ||||
| 209 | + <ul className="mt-4 flex flex-wrap gap-2" aria-label="Keywords"> | ||||
| 210 | + {v.keywords.map((k) => ( | ||||
| 211 | + <li key={k}> | ||||
| 212 | + <Link href={`/research?q=${encodeURIComponent(k)}`} className="badge no-underline"> | ||||
| 213 | + {k} | ||||
| 214 | + </Link> | ||||
| 215 | + </li> | ||||
| 216 | + ))} | ||||
| 217 | + </ul> | ||||
| 218 | + </div> | ||||
| 219 | + <aside className="surface p-4 text-sm md:col-span-4"> | ||||
| 220 | + <p className="label mb-2">Downloads</p> | ||||
| 221 | + {withdrawn ? ( | ||||
| 222 | + <p className="muted">Artifacts withdrawn.</p> | ||||
| 223 | + ) : ( | ||||
| 224 | + <ul className="space-y-2"> | ||||
| 225 | + <li> | ||||
| 226 | + <a className="btn btn-primary w-full" href={`${v.pdfPublicPath}?download=1`}> | ||||
| 227 | |||||
| 228 | + </a> | ||||
| 229 | + </li> | ||||
| 230 | + {tex.length > 0 && ( | ||||
| 231 | + <li> | ||||
| 232 | + <a | ||||
| 233 | + className="btn w-full" | ||||
| 234 | + href={`${set.find((a) => a.path === "manuscript/main.tex")?.publicPath ?? tex[0]!.publicPath}?download=1`} | ||||
| 235 | + > | ||||
| 236 | + LaTeX source | ||||
| 237 | + </a> | ||||
| 238 | + </li> | ||||
| 239 | + )} | ||||
| 240 | + {code.length > 0 && ( | ||||
| 241 | + <li> | ||||
| 242 | + <a className="btn w-full" href="#code"> | ||||
| 243 | + Code ({code.length} files) | ||||
| 244 | + </a> | ||||
| 245 | + </li> | ||||
| 246 | + )} | ||||
| 247 | + {data.length > 0 && ( | ||||
| 248 | + <li> | ||||
| 249 | + <a className="btn w-full" href="#data"> | ||||
| 250 | + Data ({data.length} files) | ||||
| 251 | + </a> | ||||
| 252 | + </li> | ||||
| 253 | + )} | ||||
| 254 | + </ul> | ||||
| 255 | + )} | ||||
| 256 | + <div className="mt-4"> | ||||
| 257 | + <Disclosure compact /> | ||||
| 258 | + </div> | ||||
| 259 | + </aside> | ||||
| 260 | + </header> | ||||
| 261 | + | ||||
| 262 | + <section aria-labelledby="abstract" className="grid gap-8 md:grid-cols-12"> | ||||
| 263 | + <div className="md:col-span-8"> | ||||
| 264 | + <h2 id="abstract" className="serif text-xl font-semibold"> | ||||
| 265 | + Abstract | ||||
| 266 | + </h2> | ||||
| 267 | + <p className="prose-paper mt-3">{v.abstract}</p> | ||||
| 268 | + {Array.isArray(meta.contributions) && (meta.contributions as string[]).length > 0 && ( | ||||
| 269 | + <> | ||||
| 270 | + <h2 className="serif mt-8 text-xl font-semibold">Key contributions</h2> | ||||
| 271 | + <ol className="prose-paper mt-3 list-decimal pl-5"> | ||||
| 272 | + {(meta.contributions as string[]).map((c) => ( | ||||
| 273 | + <li key={c}>{c}</li> | ||||
| 274 | + ))} | ||||
| 275 | + </ol> | ||||
| 276 | + </> | ||||
| 277 | + )} | ||||
| 278 | + {typeof meta.methods === "string" && meta.methods && ( | ||||
| 279 | + <> | ||||
| 280 | + <h2 className="serif mt-8 text-xl font-semibold">Methods</h2> | ||||
| 281 | + <p className="prose-paper mt-3">{meta.methods}</p> | ||||
| 282 | + </> | ||||
| 283 | + )} | ||||
| 284 | + {typeof meta.mainResults === "string" && meta.mainResults && ( | ||||
| 285 | + <> | ||||
| 286 | + <h2 className="serif mt-8 text-xl font-semibold">Main results</h2> | ||||
| 287 | + <p className="prose-paper mt-3">{meta.mainResults}</p> | ||||
| 288 | + </> | ||||
| 289 | + )} | ||||
| 290 | + {typeof meta.limitations === "string" && meta.limitations && ( | ||||
| 291 | + <> | ||||
| 292 | + <h2 className="serif mt-8 text-xl font-semibold">Limitations</h2> | ||||
| 293 | + <p className="prose-paper mt-3">{meta.limitations}</p> | ||||
| 294 | + </> | ||||
| 295 | + )} | ||||
| 296 | + </div> | ||||
| 297 | + <div className="space-y-4 md:col-span-4"> | ||||
| 298 | + <div className="surface p-4 text-sm"> | ||||
| 299 | + <p className="label mb-2">Research status</p> | ||||
| 300 | + <ul className="space-y-1"> | ||||
| 301 | + <li>AutoScholar working paper (preprint)</li> | ||||
| 302 | + <li>Produced autonomously by an AI research workflow</li> | ||||
| 303 | + <li> | ||||
| 304 | + Automated critical review; <strong>not</strong> human peer review | ||||
| 305 | + </li> | ||||
| 306 | + <li> | ||||
| 307 | + Deterministic quality gates:{" "} | ||||
| 308 | + {qa.gates | ||||
| 309 | + ? `${qa.gates.filter((g) => g.status === "pass").length}/${qa.gates.filter((g) => g.status !== "skip").length} passed` | ||||
| 310 | + : "see provenance"} | ||||
| 311 | + </li> | ||||
| 312 | + <li>DOI: none (not registered)</li> | ||||
| 313 | + </ul> | ||||
| 314 | + </div> | ||||
| 315 | + {p.summary.topics.length > 0 && ( | ||||
| 316 | + <div className="surface p-4 text-sm"> | ||||
| 317 | + <p className="label mb-2">Topics</p> | ||||
| 318 | + <ul className="flex flex-wrap gap-2"> | ||||
| 319 | + {p.summary.topics.map((x) => ( | ||||
| 320 | + <li key={x.slug}> | ||||
| 321 | + <Link href={`/topics/${x.slug}`} className="badge no-underline"> | ||||
| 322 | + {x.name} | ||||
| 323 | + </Link> | ||||
| 324 | + </li> | ||||
| 325 | + ))} | ||||
| 326 | + </ul> | ||||
| 327 | + </div> | ||||
| 328 | + )} | ||||
| 329 | + </div> | ||||
| 330 | + </section> | ||||
| 331 | + | ||||
| 332 | + {!withdrawn && ( | ||||
| 333 | + <section aria-labelledby="pdf"> | ||||
| 334 | + <h2 id="pdf" className="serif text-xl font-semibold"> | ||||
| 335 | + Read the paper | ||||
| 336 | + </h2> | ||||
| 337 | + <div className="mt-3"> | ||||
| 338 | + <PdfViewer | ||||
| 339 | + src={v.pdfPublicPath} | ||||
| 340 | + title={`${p.paper.publicId} v${v.versionNo} — ${v.title}`} | ||||
| 341 | + downloadHref={`${v.pdfPublicPath}?download=1`} | ||||
| 342 | + /> | ||||
| 343 | + </div> | ||||
| 344 | + </section> | ||||
| 345 | + )} | ||||
| 346 | + | ||||
| 347 | + {(figures.length > 0 || pdfFigures.length > 0) && ( | ||||
| 348 | + <section aria-labelledby="figures"> | ||||
| 349 | + <h2 id="figures" className="serif text-xl font-semibold"> | ||||
| 350 | + Figures | ||||
| 351 | + </h2> | ||||
| 352 | + <ul className="mt-3 grid gap-4 sm:grid-cols-2"> | ||||
| 353 | + {figures.map((f) => ( | ||||
| 354 | + <li key={f.path} className="surface p-3"> | ||||
| 355 | + {/* biome-ignore lint/performance/noImgElement: artifact images are served immutable from this origin */} | ||||
| 356 | + <img | ||||
| 357 | + src={f.publicPath} | ||||
| 358 | + alt={f.path.split("/").pop() ?? "figure"} | ||||
| 359 | + className="w-full" | ||||
| 360 | + loading="lazy" | ||||
| 361 | + /> | ||||
| 362 | + <p className="subtle mt-2 font-mono text-xs">{f.path.replace("results/figures/", "")}</p> | ||||
| 363 | + </li> | ||||
| 364 | + ))} | ||||
| 365 | + {pdfFigures.map((f) => ( | ||||
| 366 | + <li key={f.path} className="surface p-3 text-sm"> | ||||
| 367 | + <a href={`${f.publicPath}?download=1`}>{f.path.replace("results/figures/", "")}</a>{" "} | ||||
| 368 | + <span className="subtle">(vector PDF, {fmtBytes(f.bytes)})</span> | ||||
| 369 | + </li> | ||||
| 370 | + ))} | ||||
| 371 | + </ul> | ||||
| 372 | + </section> | ||||
| 373 | + )} | ||||
| 374 | + | ||||
| 375 | + {bib.length > 0 && ( | ||||
| 376 | + <section aria-labelledby="refs"> | ||||
| 377 | + <h2 id="refs" className="serif text-xl font-semibold"> | ||||
| 378 | + References ({bib.length}) | ||||
| 379 | + </h2> | ||||
| 380 | + <p className="muted mt-1 text-xs"> | ||||
| 381 | + Every reference was verified against a scholarly registry before publication; identifiers link to | ||||
| 382 | + the source of record. | ||||
| 383 | + </p> | ||||
| 384 | + <ol className="mt-3 space-y-2 text-sm"> | ||||
| 385 | + {bib.map((b) => { | ||||
| 386 | + const f = b.fields; | ||||
| 387 | + const href = f.doi | ||||
| 388 | + ? `https://doi.org/${f.doi.replace(/^https?:\/\/doi\.org\//, "")}` | ||||
| 389 | + : (f.url ?? (f.eprint ? `https://arxiv.org/abs/${f.eprint}` : undefined)); | ||||
| 390 | + return ( | ||||
| 391 | + <li key={b.key} id={`ref-${b.key}`} className="border-b pb-2 hairline"> | ||||
| 392 | + <span>{f.author ? `${f.author.replace(/\s+and\s+/g, ", ")}. ` : ""}</span> | ||||
| 393 | + {f.year && <span>({f.year}). </span>} | ||||
| 394 | + <span className="italic">{f.title?.replace(/[{}]/g, "")}</span> | ||||
| 395 | + {f.journal && <span>. {f.journal}</span>} | ||||
| 396 | + {f.booktitle && <span>. In {f.booktitle}</span>} | ||||
| 397 | + {f.volume && <span>, {f.volume}</span>} | ||||
| 398 | + {f.pages && <span>, {f.pages}</span>}.{" "} | ||||
| 399 | + {href && ( | ||||
| 400 | + <a href={href} rel="noopener noreferrer" className="font-mono text-xs break-all"> | ||||
| 401 | + {f.doi ? `doi:${f.doi}` : href} | ||||
| 402 | + </a> | ||||
| 403 | + )} | ||||
| 404 | + </li> | ||||
| 405 | + ); | ||||
| 406 | + })} | ||||
| 407 | + </ol> | ||||
| 408 | + </section> | ||||
| 409 | + )} | ||||
| 410 | + | ||||
| 411 | + {(code.length > 0 || data.length > 0 || results.length > 0) && ( | ||||
| 412 | + <section aria-labelledby="repro" className="grid gap-6 md:grid-cols-3"> | ||||
| 413 | + <h2 id="repro" className="serif text-xl font-semibold md:col-span-3"> | ||||
| 414 | + Code, data and results | ||||
| 415 | + </h2> | ||||
| 416 | + {code.length > 0 && ( | ||||
| 417 | + <div id="code" className="surface p-4"> | ||||
| 418 | + <p className="label mb-1">Code</p> | ||||
| 419 | + <ul> | ||||
| 420 | + {code.map((a) => ( | ||||
| 421 | + <Dl key={a.path} a={a} /> | ||||
| 422 | + ))} | ||||
| 423 | + </ul> | ||||
| 424 | + </div> | ||||
| 425 | + )} | ||||
| 426 | + {data.length > 0 && ( | ||||
| 427 | + <div id="data" className="surface p-4"> | ||||
| 428 | + <p className="label mb-1">Data</p> | ||||
| 429 | + <ul> | ||||
| 430 | + {data.map((a) => ( | ||||
| 431 | + <Dl key={a.path} a={a} /> | ||||
| 432 | + ))} | ||||
| 433 | + </ul> | ||||
| 434 | + </div> | ||||
| 435 | + )} | ||||
| 436 | + {results.length > 0 && ( | ||||
| 437 | + <div className="surface p-4"> | ||||
| 438 | + <p className="label mb-1">Results and tables</p> | ||||
| 439 | + <ul> | ||||
| 440 | + {results.map((a) => ( | ||||
| 441 | + <Dl key={a.path} a={a} /> | ||||
| 442 | + ))} | ||||
| 443 | + </ul> | ||||
| 444 | + </div> | ||||
| 445 | + )} | ||||
| 446 | + </section> | ||||
| 447 | + )} | ||||
| 448 | + | ||||
| 449 | + <section aria-labelledby="reproducibility" className="grid gap-6 md:grid-cols-2"> | ||||
| 450 | + <div> | ||||
| 451 | + <h2 id="reproducibility" className="serif text-xl font-semibold"> | ||||
| 452 | + Reproducibility | ||||
| 453 | + </h2> | ||||
| 454 | + <dl className="mt-3 grid grid-cols-[max-content_1fr] gap-x-4 gap-y-1 text-sm"> | ||||
| 455 | + <dt className="label">Data availability</dt> | ||||
| 456 | + <dd>{(meta.dataAvailability as string) || "Not applicable or not stated."}</dd> | ||||
| 457 | + <dt className="label">Code availability</dt> | ||||
| 458 | + <dd>{(meta.codeAvailability as string) || "Not applicable or not stated."}</dd> | ||||
| 459 | + <dt className="label">Ethics</dt> | ||||
| 460 | + <dd> | ||||
| 461 | + {(meta.ethics as string) || | ||||
| 462 | + "No restricted activities; public data, mathematics or simulations only."} | ||||
| 463 | + </dd> | ||||
| 464 | + <dt className="label">Toolchain</dt> | ||||
| 465 | + <dd className="font-mono text-xs"> | ||||
| 466 | + {prov.toolchain && Object.keys(prov.toolchain as object).length | ||||
| 467 | + ? Object.entries(prov.toolchain as Record<string, string>) | ||||
| 468 | + .map(([k, val]) => `${k} ${val}`) | ||||
| 469 | + .join(" · ") | ||||
| 470 | + : "not recorded"} | ||||
| 471 | + </dd> | ||||
| 472 | + <dt className="label">LaTeX source</dt> | ||||
| 473 | + <dd> | ||||
| 474 | + {tex.length | ||||
| 475 | + ? "Released; the PDF was rebuilt from it in a clean environment during verification." | ||||
| 476 | + : "Not released."} | ||||
| 477 | + </dd> | ||||
| 478 | + <dt className="label">Verified</dt> | ||||
| 479 | + <dd> | ||||
| 480 | + {qa.evaluatedAt ? `${fmtDate(qa.evaluatedAt)} under quality policy ${qa.policyVersion}` : "—"} | ||||
| 481 | + </dd> | ||||
| 482 | + </dl> | ||||
| 483 | + {qa.warnings && qa.warnings.length > 0 && ( | ||||
| 484 | + <details className="mt-3 text-sm"> | ||||
| 485 | + <summary className="cursor-pointer"> | ||||
| 486 | + Non-blocking warnings recorded at verification ({qa.warnings.length}) | ||||
| 487 | + </summary> | ||||
| 488 | + <ul className="muted mt-2 list-disc pl-5"> | ||||
| 489 | + {qa.warnings.map((w) => ( | ||||
| 490 | + <li key={w}>{w}</li> | ||||
| 491 | + ))} | ||||
| 492 | + </ul> | ||||
| 493 | + </details> | ||||
| 494 | + )} | ||||
| 495 | + </div> | ||||
| 496 | + <div> | ||||
| 497 | + <h2 className="serif text-xl font-semibold">Provenance</h2> | ||||
| 498 | + <dl className="mt-3 grid grid-cols-[max-content_1fr] gap-x-4 gap-y-1 text-sm"> | ||||
| 499 | + <dt className="label">Model</dt> | ||||
| 500 | + <dd className="font-mono text-xs">{String(prov.model ?? "—")}</dd> | ||||
| 501 | + <dt className="label">Reviewer model</dt> | ||||
| 502 | + <dd className="font-mono text-xs">{String(prov.reviewerModel ?? "—")}</dd> | ||||
| 503 | + <dt className="label">Instruction version</dt> | ||||
| 504 | + <dd className="font-mono text-xs"> | ||||
| 505 | + v{String(prov.instructionVersion ?? "?")} · {shortHash(prov.instructionSha256 as string)} | ||||
| 506 | + </dd> | ||||
| 507 | + <dt className="label">Manifest digest</dt> | ||||
| 508 | + <dd className="font-mono text-xs">{shortHash(prov.manifestSha256 as string)}</dd> | ||||
| 509 | + <dt className="label">Execution</dt> | ||||
| 510 | + <dd className="text-xs"> | ||||
| 511 | + {prov.startedAt | ||||
| 512 | + ? `${fmtDate(prov.startedAt as string)} → ${prov.completedAt ? fmtDate(prov.completedAt as string) : "?"}` | ||||
| 513 | + : "—"}{" "} | ||||
| 514 | + · {String(prov.infrastructure ?? "MacLustr")} | ||||
| 515 | + </dd> | ||||
| 516 | + <dt className="label">Quality gates</dt> | ||||
| 517 | + <dd> | ||||
| 518 | + {qa.gates && qa.gates.length > 0 ? ( | ||||
| 519 | + <details className="text-xs"> | ||||
| 520 | + <summary className="cursor-pointer">{qa.gates.length} gates evaluated</summary> | ||||
| 521 | + <ul className="mt-1 space-y-0.5"> | ||||
| 522 | + {qa.gates.map((g) => ( | ||||
| 523 | + <li key={g.gate}> | ||||
| 524 | + <StatusBadge status={g.status} /> <span className="font-mono">{g.gate}</span>{" "} | ||||
| 525 | + <span className="muted">— {g.message}</span> | ||||
| 526 | + </li> | ||||
| 527 | + ))} | ||||
| 528 | + </ul> | ||||
| 529 | + </details> | ||||
| 530 | + ) : ( | ||||
| 531 | + "—" | ||||
| 532 | + )} | ||||
| 533 | + </dd> | ||||
| 534 | + </dl> | ||||
| 535 | + <p className="subtle mt-2 text-xs"> | ||||
| 536 | + Artifact digests (SHA-256) are listed with each download; the full list is in | ||||
| 537 | + provenance/checksums.sha256. | ||||
| 538 | + </p> | ||||
| 539 | + </div> | ||||
| 540 | + </section> | ||||
| 541 | + | ||||
| 542 | + <section aria-labelledby="cite"> | ||||
| 543 | + <h2 id="cite" className="serif text-xl font-semibold"> | ||||
| 544 | + Cite this working paper | ||||
| 545 | + </h2> | ||||
| 546 | + <div className="mt-3"> | ||||
| 547 | + <Tabs | ||||
| 548 | + tabs={[ | ||||
| 549 | + { | ||||
| 550 | + label: "BibTeX", | ||||
| 551 | + content: ( | ||||
| 552 | + <div> | ||||
| 553 | + <pre className="surface overflow-auto p-4 text-xs">{bibtexFor(cite)}</pre> | ||||
| 554 | + <div className="mt-2 flex gap-2"> | ||||
| 555 | + <CopyButton text={bibtexFor(cite)} /> | ||||
| 556 | + <a className="btn btn-sm" href={`/research/${p.paper.slug}/cite.bib`}> | ||||
| 557 | + Download .bib | ||||
| 558 | + </a> | ||||
| 559 | + </div> | ||||
| 560 | + </div> | ||||
| 561 | + ), | ||||
| 562 | + }, | ||||
| 563 | + { | ||||
| 564 | + label: "RIS", | ||||
| 565 | + content: ( | ||||
| 566 | + <div> | ||||
| 567 | + <pre className="surface overflow-auto p-4 text-xs">{risFor(cite)}</pre> | ||||
| 568 | + <div className="mt-2 flex gap-2"> | ||||
| 569 | + <CopyButton text={risFor(cite)} /> | ||||
| 570 | + <a className="btn btn-sm" href={`/research/${p.paper.slug}/cite.ris`}> | ||||
| 571 | + Download .ris | ||||
| 572 | + </a> | ||||
| 573 | + </div> | ||||
| 574 | + </div> | ||||
| 575 | + ), | ||||
| 576 | + }, | ||||
| 577 | + { | ||||
| 578 | + label: "Plain text", | ||||
| 579 | + content: ( | ||||
| 580 | + <div> | ||||
| 581 | + <p className="surface p-4 text-sm">{plainCitationFor(cite)}</p> | ||||
| 582 | + <div className="mt-2 flex gap-2"> | ||||
| 583 | + <CopyButton text={plainCitationFor(cite)} /> | ||||
| 584 | + <a className="btn btn-sm" href={`/research/${p.paper.slug}/cite.txt`}> | ||||
| 585 | + Download .txt | ||||
| 586 | + </a> | ||||
| 587 | + </div> | ||||
| 588 | + </div> | ||||
| 589 | + ), | ||||
| 590 | + }, | ||||
| 591 | + ]} | ||||
| 592 | + /> | ||||
| 593 | + </div> | ||||
| 594 | + </section> | ||||
| 595 | + | ||||
| 596 | + <section aria-labelledby="history" className="grid gap-6 md:grid-cols-2"> | ||||
| 597 | + <div> | ||||
| 598 | + <h2 id="history" className="serif text-xl font-semibold"> | ||||
| 599 | + Version history | ||||
| 600 | + </h2> | ||||
| 601 | + <ol className="mt-3 space-y-2 text-sm"> | ||||
| 602 | + {p.versions.map((x) => ( | ||||
| 603 | + <li key={x.id} className="surface flex items-start justify-between gap-3 p-3"> | ||||
| 604 | + <div> | ||||
| 605 | + <span className="font-medium">Version {x.versionNo}</span>{" "} | ||||
| 606 | + <span className="muted">· {fmtDate(x.publishedAt)}</span> | ||||
| 607 | + <p className="muted mt-1">{x.changelog}</p> | ||||
| 608 | + </div> | ||||
| 609 | + {!withdrawn && ( | ||||
| 610 | + <a className="btn btn-sm" href={`${x.pdfPublicPath}?download=1`}> | ||||
| 611 | + PDF v{x.versionNo} | ||||
| 612 | + </a> | ||||
| 613 | + )} | ||||
| 614 | + </li> | ||||
| 615 | + ))} | ||||
| 616 | + </ol> | ||||
| 617 | + </div> | ||||
| 618 | + <div> | ||||
| 619 | + <h2 className="serif text-xl font-semibold">Corrections and notices</h2> | ||||
| 620 | + {p.corrections.length === 0 && !p.retraction ? ( | ||||
| 621 | + <p className="muted mt-3 text-sm">No corrections have been issued.</p> | ||||
| 622 | + ) : ( | ||||
| 623 | + <ul className="mt-3 space-y-2 text-sm"> | ||||
| 624 | + {p.corrections.map((c) => ( | ||||
| 625 | + <li key={c.id} className="surface p-3"> | ||||
| 626 | + <span className="font-medium"> | ||||
| 627 | + Correction v{c.fromVersionNo} → v{c.toVersionNo} | ||||
| 628 | + </span>{" "} | ||||
| 629 | + <span className="muted">· {fmtDate(c.createdAt)}</span> | ||||
| 630 | + <p className="mt-1">{c.notice}</p> | ||||
| 631 | + </li> | ||||
| 632 | + ))} | ||||
| 633 | + {p.retraction && ( | ||||
| 634 | + <li className="surface p-3"> | ||||
| 635 | + <span className="font-medium">Retraction</span>{" "} | ||||
| 636 | + <span className="muted">· {fmtDate(p.retraction.retractedAt)}</span> | ||||
| 637 | + <p className="mt-1">{p.retraction.notice}</p> | ||||
| 638 | + </li> | ||||
| 639 | + )} | ||||
| 640 | + </ul> | ||||
| 641 | + )} | ||||
| 642 | + </div> | ||||
| 643 | + </section> | ||||
| 644 | + | ||||
| 645 | + {p.related.length > 0 && ( | ||||
| 646 | + <section aria-labelledby="related"> | ||||
| 647 | + <h2 id="related" className="serif text-xl font-semibold"> | ||||
| 648 | + Related working papers | ||||
| 649 | + </h2> | ||||
| 650 | + <p className="muted mt-1 text-xs"> | ||||
| 651 | + Selected by shared discipline or keywords recorded in verified metadata. | ||||
| 652 | + </p> | ||||
| 653 | + <ul className="mt-3 grid gap-4 md:grid-cols-2"> | ||||
| 654 | + {p.related.map((r) => ( | ||||
| 655 | + <li key={r.publicId}> | ||||
| 656 | + <PaperCard p={r} /> | ||||
| 657 | + </li> | ||||
| 658 | + ))} | ||||
| 659 | + </ul> | ||||
| 660 | + </section> | ||||
| 661 | + )} | ||||
| 662 | + <Disclosure /> | ||||
| 663 | + </article> | ||||
| 664 | + ); | ||||
| 665 | +} | ||||
added
apps/web/src/app/(public)/research/page.tsx
+82 −0
@@ -0,0 +1,82 @@ | |||||
| 1 | +import { listSeries, searchPapers } from "@autoscholar/core"; | ||||
| 2 | +import { PublicSearchQuery } from "@autoscholar/schemas"; | ||||
| 3 | +import type { Metadata } from "next"; | ||||
| 4 | +import { Disclosure } from "@/components/Disclosure"; | ||||
| 5 | +import { EmptyState } from "@/components/EmptyState"; | ||||
| 6 | +import { Pagination } from "@/components/Pagination"; | ||||
| 7 | +import { PaperCard } from "@/components/PaperCard"; | ||||
| 8 | +import { SearchFilters } from "@/components/SearchFilters"; | ||||
| 9 | +import { t } from "@/lib/i18n"; | ||||
| 10 | +import { db } from "@/lib/server/db"; | ||||
| 11 | + | ||||
| 12 | +export const dynamic = "force-dynamic"; | ||||
| 13 | +export const metadata: Metadata = { | ||||
| 14 | + title: "Research library", | ||||
| 15 | + description: "Searchable library of AutoScholar working papers.", | ||||
| 16 | + alternates: { canonical: "/research" }, | ||||
| 17 | +}; | ||||
| 18 | + | ||||
| 19 | +export default async function ResearchPage({ | ||||
| 20 | + searchParams, | ||||
| 21 | +}: { | ||||
| 22 | + searchParams: Promise<Record<string, string | string[] | undefined>>; | ||||
| 23 | +}) { | ||||
| 24 | + const sp = await searchParams; | ||||
| 25 | + const raw: Record<string, string | undefined> = {}; | ||||
| 26 | + for (const [k, v] of Object.entries(sp)) raw[k] = Array.isArray(v) ? v[0] : v; | ||||
| 27 | + for (const k of Object.keys(raw)) if (raw[k] === "") delete raw[k]; | ||||
| 28 | + const parsed = PublicSearchQuery.safeParse(raw); | ||||
| 29 | + const q = parsed.success ? parsed.data : PublicSearchQuery.parse({}); | ||||
| 30 | + const d = db(); | ||||
| 31 | + const res = await searchPapers(d, q); | ||||
| 32 | + const series = await listSeries(d); | ||||
| 33 | + const hrefFor = (p: number) => { | ||||
| 34 | + const u = new URLSearchParams(); | ||||
| 35 | + for (const [k, v] of Object.entries(raw)) if (v && k !== "page") u.set(k, v); | ||||
| 36 | + u.set("page", String(p)); | ||||
| 37 | + return `/research?${u.toString()}`; | ||||
| 38 | + }; | ||||
| 39 | + const anyFilter = Object.keys(raw).some((k) => !["page", "sort", "pageSize"].includes(k)); | ||||
| 40 | + return ( | ||||
| 41 | + <div className="space-y-6"> | ||||
| 42 | + <header> | ||||
| 43 | + <p className="eyebrow">Library</p> | ||||
| 44 | + <h1 className="serif mt-1 text-3xl font-semibold">Working papers</h1> | ||||
| 45 | + <p className="muted mt-2 max-w-2xl text-sm"> | ||||
| 46 | + Search titles, abstracts, keywords and verified metadata. Filter by discipline, methodology, series, | ||||
| 47 | + availability of code and data, and publication status. | ||||
| 48 | + </p> | ||||
| 49 | + </header> | ||||
| 50 | + <SearchFilters q={raw} series={series} /> | ||||
| 51 | + {!parsed.success && ( | ||||
| 52 | + <p role="alert" className="text-sm" style={{ color: "var(--color-danger-500)" }}> | ||||
| 53 | + Some filters were invalid and have been ignored. | ||||
| 54 | + </p> | ||||
| 55 | + )} | ||||
| 56 | + <div className="flex items-center justify-between text-sm"> | ||||
| 57 | + <p className="muted" aria-live="polite"> | ||||
| 58 | + {res.total === 0 ? "No results" : `${res.total} ${res.total === 1 ? "paper" : "papers"}`} | ||||
| 59 | + {raw.q ? ` for “${raw.q}”` : ""} | ||||
| 60 | + </p> | ||||
| 61 | + <Pagination page={res.page} pageSize={res.pageSize} total={res.total} hrefFor={hrefFor} /> | ||||
| 62 | + </div> | ||||
| 63 | + {res.items.length === 0 ? ( | ||||
| 64 | + <EmptyState title={anyFilter ? t("search.empty") : "No working papers yet."}> | ||||
| 65 | + {anyFilter ? <a href="/research">Clear all filters</a> : t("library.empty")} | ||||
| 66 | + </EmptyState> | ||||
| 67 | + ) : ( | ||||
| 68 | + <ul className="grid gap-4 md:grid-cols-2"> | ||||
| 69 | + {res.items.map((p) => ( | ||||
| 70 | + <li key={p.publicId}> | ||||
| 71 | + <PaperCard p={p} /> | ||||
| 72 | + </li> | ||||
| 73 | + ))} | ||||
| 74 | + </ul> | ||||
| 75 | + )} | ||||
| 76 | + <div className="flex justify-end"> | ||||
| 77 | + <Pagination page={res.page} pageSize={res.pageSize} total={res.total} hrefFor={hrefFor} /> | ||||
| 78 | + </div> | ||||
| 79 | + <Disclosure compact /> | ||||
| 80 | + </div> | ||||
| 81 | + ); | ||||
| 82 | +} | ||||
added
apps/web/src/app/(public)/series/page.tsx
+44 −0
@@ -0,0 +1,44 @@ | |||||
| 1 | +import { listSeries } from "@autoscholar/core"; | ||||
| 2 | +import type { Metadata } from "next"; | ||||
| 3 | +import Link from "next/link"; | ||||
| 4 | +import { EmptyState } from "@/components/EmptyState"; | ||||
| 5 | +import { db } from "@/lib/server/db"; | ||||
| 6 | + | ||||
| 7 | +export const dynamic = "force-dynamic"; | ||||
| 8 | +export const metadata: Metadata = { title: "Working-paper series", alternates: { canonical: "/series" } }; | ||||
| 9 | + | ||||
| 10 | +export default async function SeriesPage() { | ||||
| 11 | + const rows = await listSeries(db()); | ||||
| 12 | + return ( | ||||
| 13 | + <div className="space-y-8"> | ||||
| 14 | + <header> | ||||
| 15 | + <p className="eyebrow">Collections</p> | ||||
| 16 | + <h1 className="serif mt-1 text-3xl font-semibold">Working-paper series</h1> | ||||
| 17 | + <p className="muted mt-2 max-w-2xl text-sm"> | ||||
| 18 | + Papers are numbered in a stable series (AS-WP-YEAR-NNNN). Numbers are allocated inside the | ||||
| 19 | + publication transaction and never reused, even after a retraction. | ||||
| 20 | + </p> | ||||
| 21 | + </header> | ||||
| 22 | + {rows.length === 0 ? ( | ||||
| 23 | + <EmptyState title="No series configured." /> | ||||
| 24 | + ) : ( | ||||
| 25 | + <ul className="grid gap-3 md:grid-cols-2"> | ||||
| 26 | + {rows.map((s) => ( | ||||
| 27 | + <li key={s.id} className="surface p-5"> | ||||
| 28 | + <Link | ||||
| 29 | + href={`/research?series=${s.slug}`} | ||||
| 30 | + className="serif text-lg font-semibold no-underline hover:underline" | ||||
| 31 | + > | ||||
| 32 | + {s.name} | ||||
| 33 | + </Link> | ||||
| 34 | + <p className="subtle mt-1 font-mono text-xs"> | ||||
| 35 | + {s.paperCount} {s.paperCount === 1 ? "paper" : "papers"} | ||||
| 36 | + </p> | ||||
| 37 | + {s.description && <p className="muted mt-2 text-sm">{s.description}</p>} | ||||
| 38 | + </li> | ||||
| 39 | + ))} | ||||
| 40 | + </ul> | ||||
| 41 | + )} | ||||
| 42 | + </div> | ||||
| 43 | + ); | ||||
| 44 | +} | ||||
added
apps/web/src/app/(public)/topics/[slug]/page.tsx
+102 −0
Large diff (102 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/(public)/topics/page.tsx
+63 −0
Large diff (63 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/(public)/transparency/page.tsx
+118 −0
Large diff (118 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/admin/(console)/LogoutButton.tsx
+25 −0
Large diff (25 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/admin/(console)/layout.tsx
+68 −0
Large diff (68 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/admin/(console)/maclustr/WorkerActions.tsx
+55 −0
Large diff (55 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/admin/(console)/maclustr/page.tsx
+106 −0
Large diff (106 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/admin/(console)/manuscripts/page.tsx
+179 −0
Large diff (179 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/admin/(console)/new/NewResearchForm.tsx
+482 −0
Large diff (482 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/admin/(console)/new/page.tsx
+49 −0
Large diff (49 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/admin/(console)/page.tsx
+148 −0
Large diff (148 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/admin/(console)/projects/[id]/page.tsx
+114 −0
Large diff (114 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/admin/(console)/projects/page.tsx
+54 −0
Large diff (54 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/admin/(console)/publications/PublicationActions.tsx
+227 −0
Large diff (227 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/admin/(console)/publications/page.tsx
+118 −0
Large diff (118 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/admin/(console)/quality/[runId]/OverrideForm.tsx
+43 −0
Large diff (43 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/admin/(console)/quality/[runId]/page.tsx
+183 −0
Large diff (183 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/admin/(console)/quality/page.tsx
+64 −0
Large diff (64 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/admin/(console)/runs/[id]/LiveTimeline.tsx
+118 −0
Large diff (118 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/admin/(console)/runs/[id]/RunActions.tsx
+79 −0
Large diff (79 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/admin/(console)/runs/[id]/page.tsx
+186 −0
Large diff (186 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/admin/(console)/runs/page.tsx
+98 −0
Large diff (98 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/admin/(console)/settings/SettingsControls.tsx
+91 −0
Large diff (91 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/admin/(console)/settings/page.tsx
+102 −0
Large diff (102 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/admin/(console)/templates/page.tsx
+70 −0
Large diff (70 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/admin/change-password/page.tsx
+98 −0
Large diff (98 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/admin/layout.tsx
+12 −0
Large diff (12 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/admin/login/page.tsx
+76 −0
Large diff (76 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/api/admin/audit/route.ts
+14 −0
Large diff (14 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/api/admin/auth/change-password/route.ts
+19 −0
Large diff (19 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/api/admin/auth/login/route.ts
+36 −0
Large diff (36 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/api/admin/auth/logout/route.ts
+17 −0
Large diff (17 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/api/admin/auth/session/route.ts
+16 −0
Large diff (16 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/api/admin/overview/route.ts
+30 −0
Large diff (30 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/api/admin/projects/[id]/instructions/route.ts
+64 −0
Large diff (64 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/api/admin/projects/[id]/route.ts
+12 −0
Large diff (12 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/api/admin/projects/route.ts
+19 −0
Large diff (19 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/api/admin/publications/[id]/metadata/route.ts
+23 −0
Large diff (23 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/api/admin/publications/[id]/retract/route.ts
+17 −0
Large diff (17 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/api/admin/publications/[id]/withdraw/route.ts
+14 −0
Large diff (14 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/api/admin/publications/route.ts
+33 −0
Large diff (33 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/api/admin/quality/[runId]/override/route.ts
+83 −0
Large diff (83 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/api/admin/quality/[runId]/route.ts
+36 −0
Large diff (36 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/api/admin/runs/[id]/artifacts/[...path]/route.ts
+40 −0
Large diff (40 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/api/admin/runs/[id]/cancel/route.ts
+12 −0
Large diff (12 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/api/admin/runs/[id]/events/route.ts
+71 −0
Large diff (71 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/api/admin/runs/[id]/retry/route.ts
+12 −0
Large diff (12 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/api/admin/runs/[id]/route.ts
+12 −0
Large diff (12 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/api/admin/runs/route.ts
+30 −0
Large diff (30 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/api/admin/settings/route.ts
+46 −0
Large diff (46 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/api/admin/templates/route.ts
+11 −0
Large diff (11 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/api/admin/workers/[id]/drain/route.ts
+15 −0
Large diff (15 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/api/admin/workers/[id]/quarantine/route.ts
+15 −0
Large diff (15 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/api/admin/workers/route.ts
+11 −0
Large diff (11 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/api/internal/workers/enroll/route.ts
+22 −0
Large diff (22 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/api/internal/workers/heartbeat/route.ts
+13 −0
Large diff (13 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/api/internal/workers/lease/route.ts
+13 −0
Large diff (13 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/api/internal/workers/runs/[id]/artifacts/route.ts
+41 −0
Large diff (41 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/api/internal/workers/runs/[id]/complete/route.ts
+14 −0
Large diff (14 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/api/internal/workers/runs/[id]/events/route.ts
+14 −0
Large diff (14 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/api/internal/workers/runs/[id]/renew/route.ts
+14 −0
Large diff (14 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/api/internal/workers/runs/[id]/stage/route.ts
+14 −0
Large diff (14 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/api/public/papers/[slug]/route.ts
+43 −0
Large diff (43 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/api/public/papers/route.ts
+16 −0
Large diff (16 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/api/public/topics/route.ts
+13 −0
Large diff (13 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/error.tsx
+22 −0
Large diff (22 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/feed.atom/route.ts
+26 −0
Large diff (26 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/feed.xml/route.ts
+22 −0
Large diff (22 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/globals.css
+282 −0
Large diff (282 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/layout.tsx
+35 −0
Large diff (35 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/not-found.tsx
+17 −0
Large diff (17 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/papers/[publicId]/[version]/[...path]/route.ts
+58 −0
Large diff (58 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/robots.ts
+10 −0
Large diff (10 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/app/sitemap.ts
+39 −0
Large diff (39 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/components/CodeBlock.tsx
+15 −0
Large diff (15 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/components/CopyButton.tsx
+24 −0
Large diff (24 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/components/DataTable.tsx
+48 −0
Large diff (48 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/components/Disclosure.tsx
+23 −0
Large diff (23 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/components/EmptyState.tsx
+10 −0
Large diff (10 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/components/MarkdownView.tsx
+8 −0
Large diff (8 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/components/Pagination.tsx
+45 −0
Large diff (45 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/components/PaperCard.tsx
+36 −0
Large diff (36 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/components/PdfViewer.tsx
+26 −0
Large diff (26 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/components/SearchFilters.tsx
+124 −0
Large diff (124 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/components/SiteFooter.tsx
+66 −0
Large diff (66 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/components/SiteHeader.tsx
+49 −0
Large diff (49 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/components/StatusBadge.tsx
+48 −0
Large diff (48 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/components/Tabs.tsx
+50 −0
Large diff (50 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/components/ThemeScript.tsx
+6 −0
Large diff (6 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/components/ThemeToggle.tsx
+31 −0
Large diff (31 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/components/Toast.tsx
+43 −0
Large diff (43 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/lib/client/api.ts
+64 −0
Large diff (64 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/lib/fonts.ts
+16 −0
Large diff (16 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/lib/format.ts
+47 −0
Large diff (47 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/lib/i18n.ts
+49 −0
Large diff (49 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/lib/server/api.ts
+68 −0
Large diff (68 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/lib/server/db.ts
+14 −0
Large diff (14 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/lib/server/internal.ts
+25 −0
Large diff (25 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/lib/server/ip.ts
+64 −0
Large diff (64 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/lib/server/markdown.ts
+53 −0
Large diff (53 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/src/lib/server/session.ts
+71 −0
Large diff (71 lines) is not loaded by default to keep the page fast.Load diff
added
apps/web/tsconfig.json
+28 −0
Large diff (28 lines) is not loaded by default to keep the page fast.Load diff
added
biome.json
+20 −0
Large diff (20 lines) is not loaded by default to keep the page fast.Load diff
added
docs/DECISIONS.md
+63 −0
Large diff (63 lines) is not loaded by default to keep the page fast.Load diff
added
docs/INFRA_DISCOVERY.md
+73 −0
Large diff (73 lines) is not loaded by default to keep the page fast.Load diff
added
docs/OPERATIONS.md
+70 −0
Large diff (70 lines) is not loaded by default to keep the page fast.Load diff
added
docs/RESEARCH_INSTRUCTIONS_GUIDE.md
+51 −0
Large diff (51 lines) is not loaded by default to keep the page fast.Load diff
added
docs/SECURITY.md
+39 −0
Large diff (39 lines) is not loaded by default to keep the page fast.Load diff
added
package.json
+37 −0
Large diff (37 lines) is not loaded by default to keep the page fast.Load diff
added
packages/auth/package.json
+21 −0
Large diff (21 lines) is not loaded by default to keep the page fast.Load diff
added
packages/auth/src/auth.test.ts
+45 −0
Large diff (45 lines) is not loaded by default to keep the page fast.Load diff
added
packages/auth/src/index.ts
+122 −0
Large diff (122 lines) is not loaded by default to keep the page fast.Load diff
added
packages/auth/tsconfig.json
+6 −0
Large diff (6 lines) is not loaded by default to keep the page fast.Load diff
added
packages/core/package.json
+28 −0
Large diff (28 lines) is not loaded by default to keep the page fast.Load diff
added
packages/core/src/admin-auth.ts
+271 −0
Large diff (271 lines) is not loaded by default to keep the page fast.Load diff
added
packages/core/src/audit.ts
+34 −0
Large diff (34 lines) is not loaded by default to keep the page fast.Load diff
added
packages/core/src/citations.ts
+52 −0
Large diff (52 lines) is not loaded by default to keep the page fast.Load diff
added
packages/core/src/env.ts
+40 −0
Large diff (40 lines) is not loaded by default to keep the page fast.Load diff
added
packages/core/src/errors.ts
+18 −0
Large diff (18 lines) is not loaded by default to keep the page fast.Load diff
added
packages/core/src/index.ts
+11 −0
Large diff (11 lines) is not loaded by default to keep the page fast.Load diff
added
packages/core/src/papers.ts
+239 −0
Large diff (239 lines) is not loaded by default to keep the page fast.Load diff
added
packages/core/src/projects.ts
+135 −0
Large diff (135 lines) is not loaded by default to keep the page fast.Load diff
added
packages/core/src/runs.ts
+393 −0
Large diff (393 lines) is not loaded by default to keep the page fast.Load diff
added
packages/core/src/settings.ts
+43 −0
Large diff (43 lines) is not loaded by default to keep the page fast.Load diff
added
packages/core/src/templates.ts
+83 −0
Large diff (83 lines) is not loaded by default to keep the page fast.Load diff
added
packages/core/src/workers.ts
+647 −0
Large diff (647 lines) is not loaded by default to keep the page fast.Load diff
added
packages/core/tsconfig.json
+6 −0
Large diff (6 lines) is not loaded by default to keep the page fast.Load diff
added
packages/db/drizzle.config.ts
+10 −0
Large diff (10 lines) is not loaded by default to keep the page fast.Load diff
added
packages/db/drizzle/0000_initial.sql
+609 −0
Large diff (609 lines) is not loaded by default to keep the page fast.Load diff
added
packages/db/drizzle/meta/0000_snapshot.json
+4393 −0
Large diff (4,394 lines) is not loaded by default to keep the page fast.Load diff
added
packages/db/drizzle/meta/_journal.json
+13 −0
Large diff (14 lines) is not loaded by default to keep the page fast.Load diff
added
packages/db/package.json
+36 −0
Large diff (36 lines) is not loaded by default to keep the page fast.Load diff
added
packages/db/src/client.ts
+35 −0
Large diff (35 lines) is not loaded by default to keep the page fast.Load diff
added
packages/db/src/index.ts
+2 −0
Large diff (2 lines) is not loaded by default to keep the page fast.Load diff
added
packages/db/src/migrate.ts
+25 −0
Large diff (25 lines) is not loaded by default to keep the page fast.Load diff
added
packages/db/src/schema/auth.ts
+93 −0
Large diff (93 lines) is not loaded by default to keep the page fast.Load diff
added
packages/db/src/schema/evidence.ts
+141 −0
Large diff (141 lines) is not loaded by default to keep the page fast.Load diff
added
packages/db/src/schema/index.ts
+9 −0
Large diff (9 lines) is not loaded by default to keep the page fast.Load diff
added
packages/db/src/schema/papers.ts
+173 −0
Large diff (173 lines) is not loaded by default to keep the page fast.Load diff
added
packages/db/src/schema/projects.ts
+68 −0
Large diff (68 lines) is not loaded by default to keep the page fast.Load diff
added
packages/db/src/schema/quality.ts
+49 −0
Large diff (49 lines) is not loaded by default to keep the page fast.Load diff
added
packages/db/src/schema/runs.ts
+163 −0
Large diff (163 lines) is not loaded by default to keep the page fast.Load diff
added
packages/db/src/schema/settings.ts
+9 −0
Large diff (9 lines) is not loaded by default to keep the page fast.Load diff
added
packages/db/src/schema/usage.ts
+47 −0
Large diff (47 lines) is not loaded by default to keep the page fast.Load diff
added
packages/db/src/schema/workers.ts
+63 −0
Large diff (63 lines) is not loaded by default to keep the page fast.Load diff
added
packages/db/src/seed.ts
+227 −0
Large diff (227 lines) is not loaded by default to keep the page fast.Load diff
added
packages/db/tsconfig.json
+5 −0
Large diff (5 lines) is not loaded by default to keep the page fast.Load diff
added
packages/maclustr-adapter/package.json
+22 −0
Large diff (22 lines) is not loaded by default to keep the page fast.Load diff
added
packages/maclustr-adapter/src/index.ts
+170 −0
Large diff (170 lines) is not loaded by default to keep the page fast.Load diff
added
packages/maclustr-adapter/tsconfig.json
+6 −0
Large diff (6 lines) is not loaded by default to keep the page fast.Load diff
added
packages/observability/package.json
+21 −0
Large diff (21 lines) is not loaded by default to keep the page fast.Load diff
added
packages/observability/src/index.ts
+52 −0
Large diff (52 lines) is not loaded by default to keep the page fast.Load diff
added
packages/observability/tsconfig.json
+6 −0
Large diff (6 lines) is not loaded by default to keep the page fast.Load diff
added
packages/research-contract/package.json
+22 −0
Large diff (22 lines) is not loaded by default to keep the page fast.Load diff
added
packages/research-contract/src/contract.test.ts
+50 −0
Large diff (50 lines) is not loaded by default to keep the page fast.Load diff
added
packages/research-contract/src/contract.ts
+58 −0
Large diff (58 lines) is not loaded by default to keep the page fast.Load diff
added
packages/research-contract/src/index.ts
+3 −0
Large diff (3 lines) is not loaded by default to keep the page fast.Load diff
added
packages/research-contract/src/instructions.ts
+124 −0
Large diff (124 lines) is not loaded by default to keep the page fast.Load diff
added
packages/research-contract/src/profiles.ts
+109 −0
Large diff (109 lines) is not loaded by default to keep the page fast.Load diff
added
packages/research-contract/tsconfig.json
+6 −0
Large diff (6 lines) is not loaded by default to keep the page fast.Load diff
added
packages/schemas/package.json
+21 −0
Large diff (21 lines) is not loaded by default to keep the page fast.Load diff
added
packages/schemas/src/admin.ts
+28 −0
Large diff (28 lines) is not loaded by default to keep the page fast.Load diff
added
packages/schemas/src/artifacts.ts
+190 −0
Large diff (190 lines) is not loaded by default to keep the page fast.Load diff
added
packages/schemas/src/events.ts
+24 −0
Large diff (24 lines) is not loaded by default to keep the page fast.Load diff
added
packages/schemas/src/index.ts
+8 −0
Large diff (8 lines) is not loaded by default to keep the page fast.Load diff
added
packages/schemas/src/modes.ts
+73 −0
Large diff (73 lines) is not loaded by default to keep the page fast.Load diff
added
packages/schemas/src/papers.ts
+41 −0
Large diff (41 lines) is not loaded by default to keep the page fast.Load diff
added
packages/schemas/src/quality.ts
+62 −0
Large diff (62 lines) is not loaded by default to keep the page fast.Load diff
added
packages/schemas/src/run.ts
+129 −0
Large diff (129 lines) is not loaded by default to keep the page fast.Load diff
added
packages/schemas/src/worker-protocol.ts
+114 −0
Large diff (114 lines) is not loaded by default to keep the page fast.Load diff
added
packages/schemas/tsconfig.json
+5 −0
Large diff (5 lines) is not loaded by default to keep the page fast.Load diff
added
packages/storage/package.json
+19 −0
Large diff (19 lines) is not loaded by default to keep the page fast.Load diff
added
packages/storage/src/index.ts
+185 −0
Large diff (185 lines) is not loaded by default to keep the page fast.Load diff
added
packages/storage/src/storage.test.ts
+46 −0
Large diff (46 lines) is not loaded by default to keep the page fast.Load diff
added
packages/storage/tsconfig.json
+6 −0
Large diff (6 lines) is not loaded by default to keep the page fast.Load diff
added
pnpm-lock.yaml
+2463 −0
Large diff (2,463 lines) is not loaded by default to keep the page fast.Load diff
Diff truncated — file too large.