Skip to content

Instantly share code, notes, and snippets.

@subtleGradient
Created June 20, 2025 18:17
Show Gist options
  • Save subtleGradient/28805e01e0efee549bd1fe5e7823c8d9 to your computer and use it in GitHub Desktop.
Save subtleGradient/28805e01e0efee549bd1fe5e7823c8d9 to your computer and use it in GitHub Desktop.

Revisions

  1. subtleGradient created this gist Jun 20, 2025.
    326 changes: 326 additions & 0 deletions CLAUDE.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,326 @@
    ### Key Architectural Patterns

    1. **Functional Pull-Oriented Dependency Injection** - Every function receives dependencies through a `deps` parameter. No imports of other functions, no side effects in pure functions.

    2. **Result Type Pattern** - Functions that can fail return `Result<T, E>`. Functions that cannot fail return values directly.

    3. **Deep Imports Only** - No barrel files. Import directly from specific files: `import {...} from '@tinkerbot/pure/git.ts'`

    4. **Workspace Protocol** - Internal dependencies use `workspace:*` protocol in package.json

    5. **Turbo Pipeline** - Build orchestration follows dependency graph:
    - `test` runs before `build`
    - `build` runs before `start`
    - `dev` depends on database being up

    ## tech

    git, typescript, bun, ai agents, markdown, react 19
    a little bit of Zig and Rust
    nix flakes
    avoid docker unless absolutely necessary

    use bun, not npm nor pnpm nor yarn
    bunx, not npx

    do not use branded string types unless it actually helps catch bugs. let's not be pedantic nuts

    no string literal types; create specific unbranded types for each conceptually different kind of string

    - barrel files are forbidden. deeply import stuff. less busy work and boilerplate
    - entrypoints may have the suffix Entrypoint e.g. createTodoAppEntrypoint so that it's obvious at a glance that it's allowed to import stuff and stuff

    ## Package Structure for Shared Code

    Reusable code is organized into three packages under `packages/`:

    ### `@tinkerbot/contracts` - Shared protocols, interfaces and types
    - **Location**: `packages/contracts/`
    - **Purpose**: All type definitions, interfaces, dependency wrappers (*Dep), and Result types
    - **Examples**: `Result<T, E>`, `ErrorWithName`, domain types, dependency interfaces
    - **Import**: `import type {...} from '@tinkerbot/contracts/result.ts'`

    ### `@tinkerbot/pure` - Pure TypeScript functions
    - **Location**: `packages/pure/`
    - **Purpose**: All pure functions with zero side effects that follow Pull-Oriented DI
    - **Examples**: Business logic, transformations, validations
    - **Import**: `import {...} from '@tinkerbot/pure/git.ts'`

    ### `@tinkerbot/pure-bun` - Bun-specific implementations
    - **Location**: `packages/pure-bun/`
    - **Purpose**: Impure code, entrypoints, and runtime-specific implementations
    - **Examples**: Application entrypoints, Bun-specific adapters
    - **Import**: `import {...} from '@tinkerbot/pure-bun/todo-app.ts'`

    Always use deep imports (no barrel files). Place new shared code in the appropriate package based on its purity and purpose.

    # Functional Pull‑Oriented Dependency Injection – Rules & Playbook

    > **Purpose** – Make every fresh contractor or AI assistant instantly productive, aligned with our zero‑surprise coding style.
    ---

    ## 🏁 TL;DR — Non‑negotiables

    1. **Pure functions only** – absolutely **no side‑effects** (I/O, logging, mutation) inside any function that's exported from a library file.
    2. **Function arguments** – if a function needs dependencies, pass a single `deps` kwargs object. If it needs no dependencies, just use normal arguments. Don't create empty deps objects.
    3. **Minimal surface**`Deps` type lists *only* what the function actually touches. Over‑providing at call‑site is fine; over‑depending is forbidden.
    4. **Zero imports** – the body may not import/require anything. Every dependency *must* come through `deps`. This includes other pure functions - they must be passed through deps, not imported. Exception: foundational types like `Result`, `ErrorWithName` from contracts are allowed.
    5. **Return shape** – Only use `Result<Good, Bad>` when failure is possible. If a function can't fail, just return the value directly.
    6. **Composable** – no singleton state, no ambient context, no service locators.
    7. **Naming** – dependency wrappers end with `Dep`; errors are `ErrorWithName<'MyError'>` or richer unions.
    8. **File endings** – implementation files live under `projects/<project>/src/`, tests mirror path under `__tests__/`.
    9. **Entrypoints** – impure, side‑effectful, never exported, invoke `process.exit` on success/failure.
    10. **Order of work** – start at the *consumer* code, then recurse into dependencies until leaf nodes are trivial.

    Keep this list on your desk. Violations block merge.

    ---

    ## 1 Principles & Rationale

    | Principle | Why it matters |
    | -------------------------- | --------------------------------------------------------------------------------------------------------- |
    | **Pure & deterministic** | Makes reasoning, caching, retries, and tests simple. |
    | **Pull, not push** | Call‑site owns wiring; no hidden global graph. |
    | \*\*Minimal \*\***`Deps`** | Reduces coupling, speeds unit tests, highlights accidental reach. |
    | **Result objects** | Forces explicit happy ☀️ / sad ☔ paths; no swallowed exceptions. |
    | **Top‑down design** | We prototype with the nicest possible API, then satisfy it layer‑by‑layer, preventing leaky abstractions. |

    ---

    ## 2 Reference Types

    ```ts
    // Always import these from your local util, never from random libs
    export type Ok<T> = { ok: true; value: T }
    export type Err<E> = { ok: false; error: E }
    export type Result<T, E> = Ok<T> | Err<E>

    export class ErrorWithName<const N extends string> extends Error {
    constructor(public name: N, ...args: ConstructorParameters<typeof Error>) {
    super(...args)
    this.name = name
    }
    }

    // Extract dependencies from a function
    export type DepsOf<F> = F extends (deps: infer D, ...args: any[]) => any ? D : never
    ```
    ---
    ## 3 Canonical Templates
    ### 3.1 Functions without dependencies
    ```ts
    /** Converts text to kebab-case */
    export const toKebabCase = (text: string): Result<string, ErrorWithName<'InvalidTaskName'>> => {
    const kebab = text
    .toLowerCase()
    .replace(/[^a-z0-9]/g, '-')
    .replace(/-+/g, '-')
    .trim()

    if (!kebab) {
    return { ok: false, error: new ErrorWithName('InvalidTaskName', 'Cannot convert empty text') } as const
    }

    return { ok: true, value: kebab } as const
    }
    ```

    ### 3.2 Direct form with dependencies (no failure possible)

    ```ts
    interface Clock { now: () => number }
    interface ClockDep { readonly clock: Clock }

    /** Returns ISO timestamp string */
    export const getNow = (deps: ClockDep): string => {
    return new Date(deps.clock.now()).toISOString()
    }

    /** Formats a timestamp - can't fail */
    export const formatTimestamp = (timestamp: number): string => {
    return new Date(timestamp).toISOString().replace(/[-:]/g, '').slice(0, 15)
    }
    ```

    ### 3.3 Curried form with dependencies

    ```ts
    interface Db { query: (sql: string) => Promise<unknown[]> }
    interface Logger { info: (...msg: unknown[]) => void }
    interface DbDep { readonly db: Db }
    interface LoggerDep { readonly logger: Logger }

    type Deps = DbDep & Partial<LoggerDep>

    export const findUserById = (deps: Deps) => async (id: string) => {
    deps.logger?.info('findUserById', id)
    try {
    const rows = await deps.db.query('SELECT * FROM users WHERE id = ?', [id])
    return { ok: true, value: rows[0] } as const
    } catch (cause) {
    return { ok: false, error: new ErrorWithName('DbError', String(cause), {cause}) } as const
    }
    }

    // When composing curried functions, prepare them once
    export const createUserService = (deps: Deps) => {
    // Prepare all functions once when deps are provided
    const findById = findUserById(deps)
    const findByEmail = findUserByEmail(deps)
    const createUser = createNewUser(deps)

    return {
    findById,
    findByEmail,
    createUser,
    // Complex operation using the prepared functions
    findOrCreate: async (email: string) => {
    const existing = await findByEmail(email)
    if (existing.ok) return existing
    return createUser({ email })
    }
    }
    }
    ```

    ### 3.4 Composing functions - the RIGHT way

    ```ts
    // WRONG - importing functions directly
    import { validateUser, hashPassword } from './auth.ts'

    export const createUser = (deps: LoggerDep) => async (email: string, password: string) => {
    const validResult = validateUser(email) // ❌ Imported function!
    const hashed = await hashPassword(password) // ❌ Imported function!
    // ...
    }

    // RIGHT - receive functions through deps
    interface AuthOperations {
    validateUser: (email: string) => Result<void, Error>
    hashPassword: (password: string) => Promise<string>
    }

    interface AuthOperationsDep {
    readonly auth: AuthOperations
    }

    export type CreateUserDeps = LoggerDep & AuthOperationsDep

    export const createUser = (deps: CreateUserDeps) => async (email: string, password: string) => {
    const validResult = deps.auth.validateUser(email) // ✅ From deps!
    if (!validResult.ok) return validResult

    const hashed = await deps.auth.hashPassword(password) // ✅ From deps!
    deps.logger.log(`Creating user: ${email}`)
    // ...
    }
    ```

    ### 3.5 Entrypoint skeleton

    ```ts
    import { createBasicLogger } from './logger'
    import { mainServer } from './server'

    if (import.meta.main) {
    const logger = createBasicLogger('app')
    try {
    await mainServer({ logger })
    process.exit(0)
    } catch (e) {
    logger.error('fatal', e)
    process.exit(1)
    }
    }
    ```

    ---

    ## 4 Workflow – "Start at the End"

    1. **Write dream call‑site** – pretend the feature already exists; sketch the nicest possible API.
    2. **Red squiggles == TODO list** – each unresolved identifier becomes a dependency you *must* supply via `deps`.
    3. **Wrap each dep** – declare `FooDep` around its interface. Add it to the parent's `Deps` intersection.
    4. **Implement leafs** – when a dep itself needs collaborators, repeat. Stop when the logic is trivial or impure.
    5. **Wire at composition root** – entrypoint or test harness constructs the full `deps` object.

    > **Never bottom‑up.** Starting with utilities leaks details upward and ruins ergonomics.
    ---

    ## 5 Testing Strategy

    * Each pure function is unit‑tested with **inline fake deps**.
    * Use object literals; no jest mocks or ts‑mock‑import nonsense.
    * Edge‑level adapters (HTTP handlers, CLI, schedulers) get integration tests only.

    ```ts
    test('getNow returns ISO string', () => {
    const fakeClock: Clock = { now: () => 0 }
    const result = getNow({ clock: fakeClock })
    expect(result).toBe('1970-01-01T00:00:00.000Z')
    })

    test('toKebabCase handles invalid input', () => {
    const result = toKebabCase('')
    expect(result.ok).toBe(false)
    expect(result.error.name).toBe('InvalidTaskName')
    })
    ```

    ---

    ## 6 Lint & CI Guards

    * **ESLint** rule `no-restricted-imports` – disallow anything except relative inside `src/`.
    * **RegEx CI check** – fails if file contains `import .* from` (except for type‑only imports in entrypoints).
    * **Type‑only deps** – runtime circular deps vanish by construction.

    ---

    ## 7 Common Pitfalls & How to Avoid Them

    | Pitfall | Fix |
    | ------------------------------------------------------ | ------------------------------------------------------------------- |
    | Leaking a `console` logger or clock via global | Pass it through deps like everything else. |
    | Throwing known error cases | Return `Err<E>` instead. Reserve `throw` for truly unexpected bugs. |
    | Adding utility functions that import `fs` or `process` | Those belong in an adapter module, never in pure logic. |
    | Importing other pure functions directly | Pass them through deps instead. No imports in pure functions! |
    | Marking every dep `Partial<>` | If it's optional you probably don't need it; rethink. |

    ---

    ## 8 FAQ

    **Q: Can I use classes?**
    A: Only for data (immutables) or typed errors. No method state.

    **Q: What about async generators / streams?**
    A: Same rules. They still accept a single `deps` object and return a `Result`‐wrapped async iterator.

    **Q: I need to mutate a cache. Side effect?**
    A: Expose a cache interface through `deps`, return the updated value; caller decides what to do.

    ---

    ## 9 Further Reading

    * Evolu DI – [https://www.evolu.dev/docs/dependency-injection](https://www.evolu.dev/docs/dependency-injection)
    * Tom's `standard-pull-di` repo – canonical source.

    ---

    **Deliver with discipline.** Any PR that diverges from these rules gets bounced. Few exceptions. 🚫

    # important-instruction-reminders
    Do what has been asked; nothing more, nothing less.
    NEVER create files unless they're absolutely necessary for achieving your goal.
    ALWAYS prefer editing an existing file to creating a new one.
    NEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested by the User.