Bun and TypeScript Coding Conventions

VibeFlow uses Bun 1.4 for development, dependency installation, builds, and tests. The published vf artifact is still a Node-targeted ESM bundle. Code must satisfy both boundaries: fast Bun-native tooling and the declared Node runtime.

These rules are executable where practical. tsconfig.json, biome.json, bun.lock, the file-size and waiver scripts, focused contract tests, and vf verify are the authorities. This page explains why those gates exist.

Runtime and modules

  • Use ESM, node: built-in imports, .js suffixes for relative source imports, and import type for type-only dependencies.
  • Source compiled into dist/cli.js must remain Node-compatible. Use a Bun-only API only behind a deliberate adapter or external build boundary with focused tests.
  • Keep TypeScript strict. The project additionally enforces isolated modules, consistent path casing, total returns, checked indexed access and side-effect imports, override markers, and switch fallthrough protection.
  • Parse external data as unknown. Prove shape, membership, bounds, and state before narrowing. New any, blind double casts, and non-null assertions require a real integration constraint and focused evidence.

Bun’s current TypeScript guidance recommends strict mode, bundler resolution, verbatimModuleSyntax, noUncheckedIndexedAccess, noImplicitOverride, and noFallthroughCasesInSwitch. VibeFlow keeps target: ES2022 instead of Bun’s ESNext suggestion because the released bundle deliberately targets Node rather than the Bun runtime. See the official Bun TypeScript guide.

Constants, unions, and hardcoded values

Do not add TypeScript enum or const enum. Biome rejects both. For closed values that must exist at runtime, use one plain-JavaScript authority and derive the type:

export const TASK_STATE = Object.freeze({
  QUEUED: "queued",
  RUNNING: "running",
  FAILED: "failed",
} as const);

export type TaskState = (typeof TASK_STATE)[keyof typeof TASK_STATE];

export const TASK_STATE_LABEL = Object.freeze({
  [TASK_STATE.QUEUED]: "Queued",
  [TASK_STATE.RUNNING]: "Running",
  [TASK_STATE.FAILED]: "Failed",
} satisfies Record<TaskState, string>);

This matches Biome’s recommendation to use JavaScript objects or TypeScript unions instead of emitted enums. See the official noEnum rule.

Centralize a value when it is a closed vocabulary, bound, field name, exit code, environment key, lifecycle state, or wire literal shared by multiple layers. Import that authority in storage, validators, services, CLI, and UI. Do not duplicate the same values as a handwritten union or create a miscellaneous constants.ts dumping ground.

A local one-use branch label, prose string, fixture payload, or UI sentence is not a protocol. Leaving it inline is clearer than manufacturing a constant with no authority.

Use the typed-protocol-contracts skill for persisted/API/config/CLI/UI vocabularies.

Structure and ownership

  • Keep ordinary source files at or below 400 lines. Split along authority and responsibility boundaries, not into arbitrary helper shards.
  • New circular dependencies are not acceptable. Contract modules stay dependency-light and browser-safe when the UI consumes them.
  • Public collections and protocol maps are immutable. Prefer Readonly, ReadonlyArray, Object.freeze, and total satisfies Record<...> projections.
  • A file-size waiver needs an issue, owner, reason, and expiry. A waiver does not replace a concrete split plan.

Processes, paths, and cleanup

  • Launch executable plus argv; keep shell: false unless a proved platform shim requires a shell. Never interpolate untrusted input into a command string.
  • Reuse the tracked launcher and owned-process abstractions instead of introducing an ad hoc Bun.spawn.
  • Persist PID plus process-start identity. PID alone is not ownership.
  • Make POSIX and Windows path semantics explicit. Avoid assuming slash, drive, UNC, symlink, reparse-point, hardlink, process-group, or signal behavior is portable.
  • Every acquired resource has deterministic success, failure, cancellation, and crash cleanup. Preserve the primary error when cleanup also fails. Bound reads, waits, retries, output, and aggregate memory.

Use runtime-portable-process-fixtures for launcher/process work and deterministic-async-teardown for lifecycle-heavy async changes.

Tests

Use bun:test with descriptive behavior names and specific matchers. Cover success, rejection, bounds, invalid external values, cleanup, and relevant platforms. Prefer table-driven cases for vocabularies and boundary matrices.

Tests must be deterministic. Do not add retries, sleeps, global state, or weaker assertions to hide a race. Native and platform adapters need injected seams locally plus a real same-SHA CI job on the target OS. Bun documents test isolation, filtering, randomized-order reproduction, teardown, and coverage in its official test runner guide.

Dependencies and reproducibility

packageManager and bun.lock define the Bun toolchain. CI uses bun ci, which is equivalent to a frozen-lockfile install and fails when declarations and the lock disagree. See the official Bun install guide.

Do not add a dependency when a small standard-library implementation is clearer. A tool upgrade must update its declaration, lockfiles, compatibility documentation, and focused contract evidence in the same change.

Required verification

This is the exact gate order CI enforces (.github/workflows/ci.yml check job); every step has failed a real PR when skipped. Run focused tests while iterating, then in order:

bun run fix                    # 1. Biome auto-format FIRST (unformatted files fail CI lint; formatting later changes test preimages)
bun run check                  # 2. typecheck + lint + file-size + waiver + whole-repo tests + 100% per-file coverage gate
bun run build                  # 3. UI (vite) + node-targeted CLI bundle; restart any local UI server with the fresh build
bunx playwright test e2e/conversation-home.spec.ts   # 4. focused e2e when the diff touches UI/components/DTOs
bun run scripts/refresh-normative-proofs.ts          # 5. after ANY test or production change (stale proof matrix fails normative:check)
git diff --check               # 6. then review evidence against the remote merge-base before pushing (pre-push hook enforces it)

vf verify is NOT a substitute for bun run check on this repo: CI’s check job runs the local scripts above, and vf verify gates vf-dispatched work. The coverage gate scans ALL of src/ (not just the diff), so an untouched file can fail a PR. Run affected UI, landing, E2E, native, or normative gates as applicable.