尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證正在載入完整 README。 以下為已儲存的摘錄。
A picture is worth a thousand words. Here's where you write them.
Community annotation for images: upload an image, draw a region on it, and attach an annotation that explains what's there.檢視完整 README ↗A picture is worth a thousand words. Here's where you write them.
Community annotation for images: upload an image, draw a region on it, and attach an annotation that explains what's there.
Live site: https://a-thousand-words-pi.vercel.app/
Status: milestone 7 (automated system tests) done. Live at https://a-thousand-words-pi.vercel.app/, with 12 annotated public-domain paintings. Signed-in users upload and annotate any image; everyone can read annotations by hovering or tapping a region; authors edit and delete their own annotations and images; every image has a "Report it" link. Sign in with GitHub to upload and annotate. Most system tests now run automatically with Playwright (npm run test:system). Next: user experience documentation (milestone 8). See the one-pager for scope and milestones.
I'm a senior software engineer with seventeen years' experience, most of it shipping front ends in regulated markets. I started this project on 1 October 2026 with four aims, and I'll keep this section updated as they progress.
Next.js (App Router, TypeScript) · Tailwind CSS · Postgres + Prisma · Cloudflare R2 · Annotorious · Auth.js · GraphQL Yoga (a read-only API)
Why each was chosen: docs/adr. How it fits together: docs/architecture.md.
Requires Node 20+ and a Postgres database.
npm install # also generates the Prisma client
cp .env.example .env # then set DATABASE_URL, the R2_* values and the AUTH_* values
npx prisma dev # optional: starts a local Postgres and prints its URL
# with it, also set SHADOW_DATABASE_URL (see .env.example)
npm run db:migrate # apply the schema
npm run db:seed -- --yes # optional: public-domain paintings and home page collections
npm run dev # http://localhost:3000
A small, read-only GraphQL API at /api/graphql returns images, their regions and annotations, and the home page collections (ADR 0023). It needs no sign-in and returns only what the site shows anyone. With npm run dev running, open http://localhost:3000/api/graphql in a browser for GraphiQL (development only), or use curl:
curl -s http://localhost:3000/api/graphql \
-H 'content-type: application/json' \
-d '{"query":"{ images(first: 2) { nodes { id title url regions { x y w h annotations { bodyMarkdown author { name } } } } pageInfo { endCursor hasNextPage } } }"}'
Pass endCursor as after for the next page. bodyMarkdown is untrusted user text: render it as the site does (ADR 0008).
| Script | What it does |
|---|---|
npm run dev | Start the dev server: port 3000 in the main checkout, a fixed port per worktree (below) |
npm run build | Production build |
npm run lint | ESLint |
npm run typecheck | TypeScript, without building (generates Next.js's route types first) |
npm test | The unit tests (Vitest, src/**/*.test.ts); npm run test:watch reruns them on change |
npm run format | Format the code with Prettier; npm run format:check only checks |
npm run test:system | The automated system tests (docs/system-tests.md): Playwright against the local database, with its own dev server and R2 mock (ADR 0024). E2E_REAL_R2=1 uses the R2 bucket from .env instead. Run npx playwright install chromium once first. |
npm run test:perf | Lighthouse CI against a production build of the home page (npm run build first, with the local database running). Warns below the performance budgets. Set CHROME_PATH to a Chrome or Chromium binary, e.g. Playwright's under ~/.cache/ms-playwright/. |
npm run db:migrate | Create and apply migrations (prisma migrate dev). Restart npm run dev afterwards: it keeps one Prisma client across hot reloads, so it won't know about new columns until restarted. |
npm run db:studio | Browse the database in Prisma Studio |
npm run db:seed | Load the seed paintings into the database and R2 from .env (dry run unless ) |
After pulling a change to prisma/schema.prisma, run npx prisma generate (or npm install) before any of the db:*, prod:* or preview:* scripts. The generated client in src/generated/prisma is git-ignored and isn't rebuilt when you pull, so a script run with the old one fails on the new models (for example, Cannot read properties of undefined (reading 'upsert')). Vercel regenerates it on every deploy.
Parallel Claude Code sessions each work in a git worktree under .claude/worktrees/. Each worktree gets its own local database and its own dev server port, so one branch's migrations and system tests don't touch another's (ADR 0020).
npm install # in the worktree
npm run worktree:setup # copies .env if missing, starts its database, migrates and seeds it
# (-- --mock: no .env; uses a local R2 mock instead, ADR 0024)
npm run dev # prints the worktree's port, between 20000 and 20999
npm run worktree:teardown # before removing the worktree
The setup writes the worktree's DATABASE_URL to .env.local, which wins over .env. The port comes from the folder name, so it's the same every time. GitHub sign-in only returns to port 3000, so check it by hand in the main checkout.
With a plan or ADR on main, one command has an agent carry it out (ADR 0025):
npm run agent:implement -- adr/0022-no-links-in-annotations
It makes the worktree and sets it up with the R2 mock, so no secrets are copied. Then it runs Claude Code headless, in auto mode, on the implement-plan skill. The agent carries out the plan, has the tests written, runs every check including the system tests, gets a code review, and opens a pull request. The pull request is a draft, with a "Needs the owner" section, when something is left for you. A desktop notification says which. The log is in .claude/agent-runs/. AGENT_BUDGET_USD caps the spend (default 25).
The site runs at https://a-thousand-words-pi.vercel.app/ on Vercel, with Neon Postgres and a production R2 bucket (ADR 0009). Pushing to main deploys.
DATABASE_URL: Neon's pooled connection stringR2_ACCOUNT_ID, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, R2_BUCKET, R2_PUBLIC_URL: the production bucket and its tokenAUTH_SECRET, AUTH_GITHUB_ID, AUTH_GITHUB_SECRET: the production OAuth appREPORT_EMAIL: where "Report it" links send emailnpm run prod:users -- approve <github-login> (run npm run prod:users to see who's waiting).npm run prod:migrate from the PR's branch before merging a PR that adds a migration. The live code ignores new columns and tables, but new code deployed before its migration fails. (A migration that removes or renames something the live code still uses needs the reverse order: first deploy code that no longer uses it.) prod:migrate uses Neon's direct connection string, from KeePassXC (below).prod:* and preview:* scripts come from KeePassXC, not from files in the project (ADR 0019). See KeePassXC entries below. Each run asks for the database's password, so run these scripts in your own terminal.infra/ deletes R2 files with no Image row once a day (ADR 0018). It deploys from main through GitHub Actions after a one-time setup.scripts/with-keepass-env.sh reads two entries from your KeePassXC database: a-thousand-words-prod for the prod:* scripts and a-thousand-words-preview for the preview:* ones.
Settings, in your shell profile (for example ~/.bashrc):
export KEEPASS_DB="$HOME/path/to/your.kdbx" # required
export KEEPASS_GROUP="Projects" # only if the entries are in a group; a path like Work/Projects works too
export KEEPASS_KEY_FILE="$HOME/path/to/keyfile" # only if the database uses a key file
The script finds an entry by its path from the root of the database. If the entries are in a group and KEEPASS_GROUP isn't set, it fails with "Could not find entry with path".
Each entry's Notes field holds one line per variable. The title must match exactly. The Password field and any custom attributes aren't read.
# Production:
# DATABASE_URL must be Neon's direct connection string
# R2 values must refer to the production bucket
DATABASE_URL="postgresql://neondb_owner:<password>@<host>.neon.tech/neondb?sslmode=verify-full&channel_binding=require"
R2_ACCOUNT_ID="<account id>"
R2_ACCESS_KEY_ID="<access key id>"
R2_SECRET_ACCESS_KEY="<secret access key>"
R2_BUCKET="a-thousand-words-prod"
R2_PUBLIC_URL="https://pub-<id>.r2.dev"
DATABASE_URL is required: the script stops without it. Use Neon's direct string (not the -pooler one), with sslmode=verify-full. The preview entry uses the preview branch's direct string.prod:seed and preview:seed also need R2_ACCOUNT_ID, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY and R2_BUCKET. A missing R2_BUCKET stops even the dry run; the other three are only read when the seed runs with --yes. R2_PUBLIC_URL isn't used by these scripts; keep it there so the entry holds the whole of production's storage settings.KEY=value, KEY="value" or KEY='value', with no spaces around =. Lines starting with #, and blank lines, are skipped. Windows line endings are fine.export prefix, a comment after a value on the same line (it becomes part of the value), escapes inside quotes, and values over more than one line.The project is set up for an AI coding agent to work on it safely (ADR 0015):
CLAUDE.md: the agent's rules, commands and definition of done..claude/settings.json: what the agent may do without asking, what it may never do (read secrets, touch production, merge, force push), and the Bash sandbox. Personal additions go in .claude/settings.local.json, which isn't committed..claude/hooks/: block edits to secrets and pushes of code to main, format each edited file, and stop the agent finishing a task while lint, typecheck or unit tests fail. The rules are strictest in git worktrees, where agents work unattended. In the main checkout, where the owner is at hand, docs (but not CLAUDE.md or AGENTS.md) can go straight to main, and failing checks are shown without holding the turn open (ADR 0021).plan/<slug> carries out docs/plans/<slug>.md, and adr/<number>-<slug> carries out an ADR. Either must already be on main. fix/, docs/ and dependabot/ branches need neither. CI checks every pull request's branch..claude/skills/: write-adr records a decision in the house format; /release <PR> takes a pull request to production in the right order (migrate, merge, deploy, production checks), with the owner running the production steps (ADR 0017)..claude/agents/: a code-reviewer that can only read, and a test-writer that can only edit test files. A hook enforces both..mcp.json: a headless browser, Playwright MCP, so the agent can look at the pages it changes. It's signed in as a local user, "Agent (local)", and can open only the checkout's own dev server and R2 mock, and where images are served from. Tools that run JavaScript are denied, and the mcp-guard.mjs hook checks every call, since MCP servers run outside the sandbox (ADR 0026)..github/workflows/ci.yml: branch name, format check, lint, typecheck, unit tests, build and dependency audit on every pull request. The repository ruleset requires it to pass before a merge.-- --yesnpm run db:users | List accounts waiting for approval; -- approve <github-login> approves one |
npm run worktree:setup | In a git worktree: give it its own local database, migrated and seeded (below) |
npm run worktree:teardown | In a git worktree, before removing it: delete its local database |
npm run agent:implement -- <branch> | Have an agent carry out a plan or ADR from main in a new worktree, unattended, through to a pull request (below) |
npm run prod:migrate | Apply migrations to production, with settings from KeePassXC (see Deploying) |
npm run prod:seed | Seed production, with settings from KeePassXC (dry run unless -- --yes) |
npm run preview:migrate | Apply migrations to the preview database, with settings from KeePassXC |
npm run preview:seed | Seed the preview database, with settings from KeePassXC (dry run unless -- --yes) |
npm run prod:users | List or approve accounts on production, with settings from KeePassXC |