```ts
/**
 * Returns the smallest start of a run of length `free` blocks lying entirely below the capacity,
 * or -1 when none exists.
 */
export function findFreeRun(words: number[], bits: number, length: number): number {
    if (!Number.isInteger(length) || length <= 0) {
        throw new Error('Length must be a positive integer');
    }
    const wordCount = Math.ceil(bits / 32);
    if (words.length !== wordCount) {
        throw new Error(`Word count (${words.length}) does not match capacity (${wordCount})`);
    }

    for (let start = 0; start <= bits - length; ++start) {
        let occupied = false;
        for (let i = start; i < start + length && !occupied; ++i) {
            const wordIndex = Math.floor(i / 32);
            const bitIndex = i % 32;
            if (words[wordIndex] & (1 << bitIndex)) {
                occupied = true;
            }
        }
        if (!occupied) {
            return start;
        }
    }

    return -1;
}

/**
 * Returns a new word list with the run of length `length` blocks starting at `start` marked occupied.
 */
export function occupyRun(words: number[], start: number, length: number): number[] {
    const result = words.slice();
    for (let i = start; i < start + length; ++i) {
        if (!Number.isInteger(i) || i < 0 || i >= bits) {
            throw new Error('Index out of bounds');
        }
        const wordIndex = Math.floor(i / 32);
        const bitIndex = i % 32;
        result[wordIndex] |= 1 << bitIndex;
    }

    return result;
}

/**
 * Returns the value of bit at `index` as 0 or 1.
 */
export function bitAt(words: number[], index: number): number {
    if (!Number.isInteger(index) || index < 0 || index >= bits) {
        throw new Error('Index out of bounds');
    }
    const wordIndex = Math.floor(index / 32);
    const bitIndex = index % 32;
    return (words[wordIndex] >> bitIndex) & 1;
}
```