Skip to content

chore(infra): make a fresh clone startable (#315) - #333

Merged
jayesh-keychain merged 1 commit into
mainfrom
fix/315-fresh-clone-bootstrap
Aug 3, 2026
Merged

chore(infra): make a fresh clone startable (#315)#333
jayesh-keychain merged 1 commit into
mainfrom
fix/315-fresh-clone-bootstrap

Conversation

@jayesh-keychain

Copy link
Copy Markdown
Collaborator

What and why

A clean clone of TheBrain could not reach a working local stack. #315 catalogued
seven manual fixes standing in the way; one (the configurable Postgres host port)
landed separately in #316, so this closes the remaining six — plus the docs so the
next person doesn't rediscover them.

Phase 1's exit gate (#204) is now dogfooding-blocked, not code-blocked — four
of its five §13.2 criteria close only by running the system over time. That makes
"can you start the system at all" the highest-leverage thing in the repo, which is
why this is P1 rather than housekeeping.

The six fixes

# Was Now
1 No .env anywhere; apps/api/.env.example shipped <generate-with-openssl-rand-hex-32> placeholders that only failed env.ts's min(8) after boot pnpm setup:env writes all three env files and substitutes real 32-byte hex secrets. Idempotent — never overwrites, so re-running preserves your API keys
3 Node ≥22 required, but pnpm downgraded the mismatch to a soft Unsupported engine warning and installed anyway .nvmrc pins 22; engine-strict=true makes it a hard, named failure
4 pnpm dev on a clean checkout died with ERR_MODULE_NOT_FOUND naming an app file, reading as a broken import turbo's dev task gains dependsOn: ["^build"]
5 Android build crashed with NoSuchFieldError: JvmVendorSpec ... IBM_SEMERU — naming neither cause nor fix toolchain auto-provisioning disabled, so a missing JDK 17 fails with Gradle's own actionable message. apps/mobile/README.md documents JDK 17 + the exact JAVA_HOME invocation
6 A fresh DB had zero admins and no supported way to make one; the documented workaround was UPDATE users SET role='admin' … — the exact invariant §2.1 bypass pnpm --filter @thebrain/api promote -- <email> <role> goes through adminRepository.updateRole; SEED_ROLE does the same at seed time. Both refuse to run in production
7 The seed bcrypted the literal demo (4 chars), which registerRequestSchema rejects (min(8)) — the account was usable but not reproducible through any public endpoint password is now ≥8 chars, asserted against the real zod contract

Finding 7 turned out to exist in two places: scripts/seed.ts and
src/dev/seed-synthetic.ts (seed:persona) each hardcoded the same 4-char
literal for the same email. Both now share one DEMO_CREDENTIALS constant, so
they can't drift apart again.

Finding 2's compose change was already on main; what was missing was any mention
of POSTGRES_PORT in the env examples, so nobody could discover the knob. Added.

One deliberate deviation from the issue

#315 suggested pinning foojay-resolver-convention ≥1.0.0 via pluginManagement
in apps/mobile/android/settings.gradle. That cannot work. The 0.5.0 pin lives
in @react-native/gradle-plugin/settings.gradle.kts, which is pulled in with
includeBuild(...) — an included build resolves plugins through its own
pluginManagement scope, so the consuming project cannot override it. (It's also
a node_modules file, so editing it isn't durable.)

Disabling toolchain auto-provisioning attacks the actual trigger instead: the
resolver is only ever invoked to auto-download a missing JDK 17, so with
auto-download off it is never touched, and the failure becomes Gradle's own
"No matching toolchain ... auto provisioning is disabled".

Acceptance criteria

  • Clean clone + pnpm install && pnpm setup:env && pnpm dev brings up API + admin, zero manual edits — verified by a from-scratch run.
    git clone of this branch into an empty dir → pnpm install (6s) → pnpm setup:env (3 files, secrets generated) → pnpm dev. Result: @thebrain/types:build: cache miss, executing (a real compile, not a cache replay), then listening on http://localhost:4000/api and admin Ready in 394ms. GET /api/health → 200; POST /api/auth/register201 with a JWT signed by the generated secret; GET localhost:3000 → 200 <title>TheBrain — Admin</title>, renders with zero console errors.
  • docker-compose.yml uses ${POSTGRES_PORT:-5432}; with POSTGRES_PORT=5433, all three containers healthy.
    Compose change landed in chore(infra): make the postgres host port configurable (#315) #316; verified live here — postgres: healthy (5432→5433), redis: healthy, nats: healthy. This PR adds the POSTGRES_PORT documentation the examples were missing.
  • .nvmrc pins 22 and engine-strict=true; Node 20 fails immediately naming the required version.
    Under v20.19.4: ERR_PNPM_UNSUPPORTED_ENGINE … Expected version: >=22 Got: v20.19.4, exit code 1.
  • pnpm dev succeeds with packages/*/dist deleted (no ERR_MODULE_NOT_FOUND).
    Proven in both directions. Fix removed → reproduces the issue's error verbatim: Cannot find module '.../@thebrain/types/dist/index.js' imported from .../encode-queue.ts. Fix in place → types#build/core#build run first, API boots, grep -c ERR_MODULE_NOT_FOUND = 0. Task graph confirms @thebrain/api#dev ← @thebrain/types#build, @thebrain/core#build.
  • A documented command creates the first admin and a superadmin without raw SQL; /ops Runtime Config and Feature Flags render instead of "Insufficient role".
    pnpm seedrole: user, then pnpm promote -- demo@thebrain.dev superadminuser → superadmin (confirmed in Postgres). As superadmin: GET /api/admin/config 200, GET /api/admin/flags 200 (real entries). Demoted the same user to admin: both 403 — the "Insufficient role" the AC names. Re-running promote is idempotent; bad role, unknown user, no-args, and NODE_ENV=production are all rejected with actionable messages.
  • Mobile README states the JDK 17 requirement and the exact build command; a clean pnpm android succeeds with only JDK 17 + Android SDK.
    README written (first half done). The build itself is unverified — this machine has no JDK and no Android SDK (/usr/libexec/java_home -V → "Unable to locate a Java Runtime"), so pnpm android could not be run at all. The gradle.properties change is reasoned from the RN/Gradle sources quoted above, not from a green build. Flagging rather than ticking; needs a run on a machine with the Android toolchain.
  • CLAUDE.md gains a short "fresh machine" section.
    Added, and mirrored verbatim into AGENTS.md (the Codex twin) so the two don't drift.

Verification summary

pnpm type-check   7 successful, 7 total
pnpm lint         7 successful, 7 total   (0 errors, 0 warnings)
pnpm build        5 successful, 5 total
pnpm test         5 successful, 5 total  —  141 files, 989 tests passed, 0 failed
                    @thebrain/api     128 files / 842 tests
                    @thebrain/core      6 files /  90 tests
                    @thebrain/admin     7 files /  57 tests

New tests — 16 of the 989, TDD red→green (both files failed with
Cannot find module before the implementations existed):

  • apps/api/src/dev/promote.test.ts — the argument contract and production guard.
    Includes a regression test for the -- separator: pnpm … promote -- a@b.c admin
    forwards -- into argv, so the documented invocation failed until handled.
    Caught by running the command rather than by reading it.
  • apps/api/src/dev/seed-credentials.test.ts — asserts the demo credentials satisfy
    registerRequestSchema/loginRequestSchema, so finding 7 cannot regress silently,
    plus SEED_ROLE parsing (defaults to user, rejects typos before any write).

Proven by tests vs. by running (per M11): findings 6 and 7 have real unit tests
because they're logic. Findings 1, 3, 4 and the AC1 cold start are config/tooling —
verified by running them and capturing the output above, including a deliberate
revert of the turbo change to confirm the fix is load-bearing. Finding 5 is
documented and reasoned but not verified.

Invariant check

  • No new queries; promote reuses adminRepository.updateRole + authRepository.findByEmail. No raw SQL — removing a raw-SQL workaround is the point of finding 6.
  • No off-box calls added, so the §2.2 fence is untouched.
  • No time-dependent logic; no migrations; no new external deps (setup:env uses only node:crypto/node:fs).
  • SEED_ROLE declared in turbo.json globalEnv (lint caught the omission).
  • Both new scripts refuse to run under NODE_ENV=production.

Closes #315

Co-authored-by: Claude noreply@anthropic.com

Seven undocumented manual fixes stood between a clean clone and a working
local stack. Six remained (the configurable Postgres host port landed in
#316); this closes them.

- `pnpm setup:env` writes .env, apps/api/.env and apps/mobile/.env from their
  examples and generates real JWT secrets, replacing the `<generate-with-...>`
  placeholders that only failed env.ts's `min(8)` AFTER boot. Idempotent.
- `.nvmrc` pins 22 and `engine-strict=true` turns pnpm's soft `Unsupported
  engine` warning into a hard failure naming the required version.
- turbo's `dev` task gains `dependsOn: ["^build"]`, so @thebrain/types and
  @thebrain/core compile before any app boots instead of dying with an
  ERR_MODULE_NOT_FOUND that names an *app* file.
- `pnpm --filter @thebrain/api promote -- <email> <role>` bootstraps the first
  admin/superadmin through `adminRepository.updateRole` rather than the raw
  `UPDATE users SET role=…` that invariant §2.1 forbids; `SEED_ROLE` does the
  same at seed time. Both refuse to run in production.
- the seeded demo password is now >= 8 chars in BOTH seed paths (`seed` and
  `seed:persona`, which had the same 4-char literal), so the account satisfies
  `registerRequestSchema` and is reproducible through the public API.
- Android toolchain auto-provisioning is disabled, so a missing JDK 17 fails
  with Gradle's actionable "no matching toolchain" message instead of the
  foojay resolver's `NoSuchFieldError: ... IBM_SEMERU`.

Docs: a "fresh machine" section in CLAUDE.md/AGENTS.md, a new
apps/mobile/README.md covering the JDK 17 requirement, and the `dev` skill now
calls setup:env instead of exiting when .env is absent.

Closes #315

Co-authored-by: Claude <noreply@anthropic.com>
@jayesh-keychain
jayesh-keychain merged commit 023127a into main Aug 3, 2026
6 checks passed
@jayesh-keychain
jayesh-keychain deleted the fix/315-fresh-clone-bootstrap branch August 3, 2026 18:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

chore(infra): a fresh clone can't start — 7 undocumented manual fixes block local dev, mobile, and admin

2 participants