/** * Pure helpers for the per-worktree environment provisioning in * `scripts/worktree.mjs` — deterministic port + database names derived from * the branch, so parallel agents (each in their own worktree) never fight * over port 3000, the shared `teqo_test` database, or a second Postgres * container on 5432. * * Contract: `slot` is the single collision domain. Port and BOTH database * names derive from the final slot, so a bumped slot frees all three at * once. Deterministic for a given (branch, code, live taken-slots set) — * the same "depends on live state" determinism the claim-queue branch picker * already has. */ /** Dev-server ports start here: slot 15 → http://localhost:3115. */ export const DEV_PORT_BASE = 3100 /** Marker line identifying a generated env file (never overwrite manual ones). */ export const GENERATED_ENV_MARKER = '# teqo worktree env (generated by pnpm worktree next)' /** Slot cap — ports above 3100 + 999 would leave the usual ephemeral range. */ const MAX_SLOT = 999 /** First run of digits in the issue code (`C15` → 15, `B164` → 164); null when none. */ export const numericSlotOfCode = (code) => { const match = /([0-9]+)/.exec(code ?? '') return match ? Number(match[1]) : null } /** Stable 32-bit FNV-1a hash → slot in [0, MAX_SLOT] for codes without digits. */ export const hashSlotOf = (value) => { let hash = 0x811c9dc5 for (const byte of Buffer.from(String(value), 'utf8')) { hash ^= byte hash = Math.imul(hash, 0x01000193) >>> 0 } return hash % (MAX_SLOT + 1) } /** The slot a branch prefers before collision resolution. */ const preferredSlotForBranch = (branch, code) => { const numeric = numericSlotOfCode(code) return numeric !== null && numeric <= MAX_SLOT ? numeric : hashSlotOf(branch) } export const devPortForSlot = (slot) => DEV_PORT_BASE + slot export const devDatabaseForSlot = (slot) => `teqo_wt${slot}` export const testDatabaseForSlot = (slot) => `teqo_wt${slot}_test` /** DB name charset guard — derived names are fed into `CREATE DATABASE` SQL. */ export const isGeneratedDatabaseName = (name) => /^teqo_wt[0-9]+(_test)?$/.test(name ?? '') /** * Final environment for a branch. `takenSlots` = slots already claimed by * OTHER live worktrees; bumps +1 until free (bounded — live slot sets are * tiny). Every value a consumer needs derives from the one final slot. */ export const worktreeEnvironment = ({ branch, code, takenSlots = new Set() }) => { let slot = preferredSlotForBranch(branch, code) while (takenSlots.has(slot)) slot += 1 return { slot, devPort: devPortForSlot(slot), devDatabase: devDatabaseForSlot(slot), testDatabase: testDatabaseForSlot(slot), } }