Files
dc-recorder/docs/superpowers/plans/2026-05-13-aggressive-cleanup.md
2026-05-13 15:28:25 +07:00

25 KiB

Aggressive Cleanup Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Clean up Bun/TypeScript Discord voice recorder with Biome, Vitest, stricter types, modular recorder code, and verification scripts.

Architecture: Keep runtime behavior same while moving pure logic into focused modules. src/recorder.ts remains public API or re-exports recorder module to minimize import churn; new src/recorder/* files own config parsing, metadata creation, decoder lifecycle, segment lifecycle, and stream orchestration.

Tech Stack: Bun, TypeScript strict mode, Biome, Vitest, discord.js-selfbot-v13, @discordjs/voice, prism-media.


File Map

  • Modify package.json: add scripts and dev dependencies.
  • Create biome.json: formatter/linter config.
  • Create vitest.config.ts: Bun-compatible Vitest config.
  • Modify tsconfig.json: include tests/config if needed, keep strict mode.
  • Modify src/config.ts: typed env/config parsing.
  • Create src/types.ts: shared recorder-facing types.
  • Create src/recorder/metadata.ts: user metadata and segment metadata builders.
  • Create src/recorder/decoder.ts: Opus decoder lifecycle.
  • Create src/recorder/segment.ts: OGG segment lifecycle.
  • Create src/recorder/audioStream.ts: subscribe/event wiring helpers.
  • Modify src/recorder.ts: orchestrator using new modules.
  • Create tests/config.test.ts, tests/recorder/metadata.test.ts, tests/recorder/decoder.test.ts, tests/recorder/segment.test.ts.

Task 1: Tooling setup

Files:

  • Modify: package.json

  • Create: biome.json

  • Create: vitest.config.ts

  • Modify: tsconfig.json

  • Step 1: Add dependencies

Run:

bun add -d @biomejs/biome vitest

Expected: package.json and lockfile update.

  • Step 2: Update scripts in package.json

Set scripts to include:

{
  "dev": "bun --watch src/index.ts",
  "start": "bun src/index.ts",
  "typecheck": "tsc --noEmit",
  "lint": "biome check .",
  "format": "biome format --write .",
  "test": "vitest run"
}
  • Step 3: Create biome.json
{
  "$schema": "https://biomejs.dev/schemas/2.0.0/schema.json",
  "files": {
    "includes": ["src/**/*.ts", "tests/**/*.ts", "*.json", "*.ts"]
  },
  "formatter": {
    "enabled": true,
    "indentStyle": "space",
    "indentWidth": 2
  },
  "linter": {
    "enabled": true,
    "rules": {
      "recommended": true,
      "suspicious": {
        "noExplicitAny": "warn"
      }
    }
  }
}
  • Step 4: Create vitest.config.ts
import { defineConfig } from "vitest/config";

export default defineConfig({
  test: {
    environment: "node",
    include: ["tests/**/*.test.ts"],
  },
});
  • Step 5: Verify tooling commands

Run:

bun run typecheck
bun run lint
bun run test

Expected: typecheck may pass or show existing issues; lint may show format/type warnings; test may pass with no tests or fail if Vitest needs config adjustment. Fix only setup errors in this task.

  • Step 6: Commit
git add package.json bun.lockb biome.json vitest.config.ts tsconfig.json
git commit -m "chore: add code quality tooling"

Task 2: Config parsing tests and implementation

Files:

  • Modify: src/config.ts

  • Create: tests/config.test.ts

  • Step 1: Write failing tests

Create tests/config.test.ts:

import { describe, expect, it } from "vitest";
import { parseBoolean, parsePositiveNumber } from "../src/config";

describe("config parsers", () => {
  it("parses boolean values", () => {
    expect(parseBoolean("true", false)).toBe(true);
    expect(parseBoolean("false", true)).toBe(false);
    expect(parseBoolean(undefined, true)).toBe(true);
  });

  it("parses positive numbers", () => {
    expect(parsePositiveNumber("5000", 0)).toBe(5000);
    expect(parsePositiveNumber("0", 123)).toBe(123);
    expect(parsePositiveNumber("bad", 123)).toBe(123);
    expect(parsePositiveNumber(undefined, 123)).toBe(123);
  });
});
  • Step 2: Run failing test

Run:

bun run test tests/config.test.ts

Expected: FAIL because parseBoolean and parsePositiveNumber are not exported.

  • Step 3: Implement config helpers

Update src/config.ts to export:

export interface AppConfig {
  verbose: boolean;
  recordingsDir: string;
  recordingSegmentMs: number;
  decoderRotateMs: number;
  decoderCooldownMs: number;
}

export function parseBoolean(value: string | undefined, fallback: boolean): boolean {
  if (value === "true") return true;
  if (value === "false") return false;
  return fallback;
}

export function parsePositiveNumber(value: string | undefined, fallback: number): number {
  const parsed = Number(value);
  return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
}

export function loadConfig(env: NodeJS.ProcessEnv = process.env): AppConfig {
  return {
    verbose: parseBoolean(env.VERBOSE, false),
    recordingsDir: env.RECORDINGS_DIR ?? "./recordings",
    recordingSegmentMs: parsePositiveNumber(env.RECORDING_SEGMENT_MS, 5_000),
    decoderRotateMs: parsePositiveNumber(env.DECODER_ROTATE_MS, 5_000),
    decoderCooldownMs: 30_000,
  };
}

export const config = loadConfig();

Preserve any existing exports by folding them into AppConfig if needed.

  • Step 4: Run config tests

Run:

bun run test tests/config.test.ts

Expected: PASS.

  • Step 5: Run typecheck

Run:

bun run typecheck

Expected: PASS or only unrelated existing errors. Fix config-related errors.

  • Step 6: Commit
git add src/config.ts tests/config.test.ts
git commit -m "refactor: type application config"

Task 3: Shared recorder types

Files:

  • Create: src/types.ts

  • Step 1: Create shared types

Create src/types.ts:

import type fs from "node:fs";
import type prism from "prism-media";

export interface RoleMetadata {
  id: string;
  name: string;
  position: number;
}

export interface UserMetadata {
  userId: string;
  username: string;
  tag: string;
  displayName: string;
  avatarUrl: string;
  bot: boolean;
  roles: RoleMetadata[];
  highestRole: RoleMetadata | null;
  joinedTimestamp: number | null;
}

export interface SegmentState {
  index: number;
  startTime: number;
  endTime: number | null;
  filename: string;
  jsonFilename: string;
  oggStream: prism.opus.OggLogicalBitstream;
  out: fs.WriteStream;
}

export interface SegmentMetadata extends UserMetadata {
  sessionId: string;
  sessionStartTime: number;
  segmentIndex: number;
  segmentMs: number;
  startTime: number;
  endTime: number;
  durationMs: number;
  filename: string;
}

export interface PcmBroadcaster {
  broadcastPcmToWeb?: (chunk: Buffer, userId: string) => void;
  updateActiveUser?: (userId: string, data: { username: string; avatar: string; speaking: boolean }) => void;
}
  • Step 2: Run typecheck

Run:

bun run typecheck

Expected: PASS. If prism type export fails, use unknown for oggStream plus local narrowed calls in segment implementation.

  • Step 3: Commit
git add src/types.ts
git commit -m "refactor: add recorder domain types"

Task 4: Metadata tests and implementation

Files:

  • Create: src/recorder/metadata.ts

  • Create: tests/recorder/metadata.test.ts

  • Step 1: Write tests

Create tests/recorder/metadata.test.ts:

import { describe, expect, it } from "vitest";
import { createSegmentMetadata } from "../../src/recorder/metadata";
import type { SegmentState, UserMetadata } from "../../src/types";

const user: UserMetadata = {
  userId: "123",
  username: "alice",
  tag: "alice#0001",
  displayName: "Alice",
  avatarUrl: "https://cdn.discordapp.com/embed/avatars/0.png",
  bot: false,
  roles: [{ id: "role", name: "Admin", position: 1 }],
  highestRole: { id: "role", name: "Admin", position: 1 },
  joinedTimestamp: 100,
};

const segment = {
  index: 2,
  startTime: 1_000,
  endTime: 2_500,
  filename: "/tmp/2500.ogg",
  jsonFilename: "/tmp/2500.json",
  oggStream: {} as SegmentState["oggStream"],
  out: {} as SegmentState["out"],
};

describe("createSegmentMetadata", () => {
  it("combines user and segment data", () => {
    const metadata = createSegmentMetadata(user, segment, "session-1", 900, 5_000);

    expect(metadata).toMatchObject({
      userId: "123",
      username: "alice",
      sessionId: "session-1",
      sessionStartTime: 900,
      segmentIndex: 2,
      segmentMs: 5_000,
      startTime: 1_000,
      endTime: 2_500,
      durationMs: 1_500,
      filename: "2500.ogg",
    });
  });
});
  • Step 2: Run failing test
bun run test tests/recorder/metadata.test.ts

Expected: FAIL because module does not exist.

  • Step 3: Implement metadata module

Create src/recorder/metadata.ts:

import path from "node:path";
import type { Client, VoiceChannel } from "discord.js-selfbot-v13";
import type { SegmentMetadata, SegmentState, UserMetadata } from "../types";

export async function collectUserMetadata(
  client: Client,
  userId: string,
  channel: VoiceChannel,
): Promise<UserMetadata> {
  const user = client.users.cache.get(userId) ?? (await client.users.fetch(userId).catch(() => null));
  const member = channel.guild.members.cache.get(userId) ?? (await channel.guild.members.fetch(userId).catch(() => null));
  const username = user?.username ?? "Unknown User";
  const roles =
    member?.roles.cache
      .filter((role) => role.id !== channel.guild.id)
      .sort((a, b) => b.position - a.position)
      .map((role) => ({ id: role.id, name: role.name, position: role.position })) ?? [];

  return {
    userId,
    username,
    tag: user?.tag ?? "Unknown#0000",
    displayName: member?.displayName ?? username,
    avatarUrl: user?.displayAvatarURL({ format: "png", size: 64 }) ?? "https://cdn.discordapp.com/embed/avatars/0.png",
    bot: user?.bot ?? false,
    roles,
    highestRole: roles[0] ?? null,
    joinedTimestamp: member?.joinedTimestamp ?? null,
  };
}

export function createSegmentMetadata(
  user: UserMetadata,
  segment: SegmentState,
  sessionId: string,
  sessionStartTime: number,
  recordingSegmentMs: number,
): SegmentMetadata {
  const endTime = segment.endTime ?? Date.now();
  return {
    ...user,
    sessionId,
    sessionStartTime,
    segmentIndex: segment.index,
    segmentMs: recordingSegmentMs,
    startTime: segment.startTime,
    endTime,
    durationMs: endTime - segment.startTime,
    filename: path.basename(segment.filename),
  };
}
  • Step 4: Run tests and typecheck
bun run test tests/recorder/metadata.test.ts
bun run typecheck

Expected: PASS.

  • Step 5: Commit
git add src/recorder/metadata.ts tests/recorder/metadata.test.ts
git commit -m "refactor: extract recorder metadata builders"

Task 5: Decoder tests and implementation

Files:

  • Create: src/recorder/decoder.ts

  • Create: tests/recorder/decoder.test.ts

  • Step 1: Write tests using fake decoder factory

Create tests/recorder/decoder.test.ts:

import { describe, expect, it, vi } from "vitest";
import { OpusDecoder } from "../../src/recorder/decoder";

class FakeDecoder {
  handlers = new Map<string, (...args: unknown[]) => void>();
  destroyed = false;
  writes: Buffer[] = [];

  on(event: string, handler: (...args: unknown[]) => void) {
    this.handlers.set(event, handler);
    return this;
  }

  write(chunk: Buffer) {
    this.writes.push(chunk);
  }

  removeAllListeners() {
    this.handlers.clear();
  }

  destroy() {
    this.destroyed = true;
  }
}

describe("OpusDecoder", () => {
  it("creates decoder lazily and writes chunks", () => {
    const fake = new FakeDecoder();
    const decoder = new OpusDecoder({ cooldownMs: 30_000, rotateMs: 5_000, createDecoder: () => fake as never, onData: vi.fn() });

    decoder.write(Buffer.from([1, 2, 3]));

    expect(fake.writes).toHaveLength(1);
  });

  it("destroys and recreates after rotation timeout", () => {
    vi.useFakeTimers();
    const created: FakeDecoder[] = [];
    const decoder = new OpusDecoder({
      cooldownMs: 30_000,
      rotateMs: 5_000,
      createDecoder: () => {
        const fake = new FakeDecoder();
        created.push(fake);
        return fake as never;
      },
      onData: vi.fn(),
    });

    decoder.write(Buffer.from([1]));
    vi.advanceTimersByTime(5_001);
    decoder.rotateIfNeeded();
    decoder.write(Buffer.from([2]));

    expect(created).toHaveLength(2);
    expect(created[0].destroyed).toBe(true);
    vi.useRealTimers();
  });
});
  • Step 2: Run failing test
bun run test tests/recorder/decoder.test.ts

Expected: FAIL because module does not exist.

  • Step 3: Implement decoder module

Create src/recorder/decoder.ts:

import prism from "prism-media";

export interface OpusDecoderOptions {
  cooldownMs: number;
  rotateMs: number;
  createDecoder?: () => prism.opus.Decoder;
  onData: (pcm: Buffer) => void;
}

export class OpusDecoder {
  private decoder: prism.opus.Decoder | null = null;
  private disabledUntil = 0;
  private createdAt = 0;
  private readonly cooldownMs: number;
  private readonly rotateMs: number;
  private readonly createDecoderFn: () => prism.opus.Decoder;
  private readonly onData: (pcm: Buffer) => void;

  constructor(options: OpusDecoderOptions) {
    this.cooldownMs = options.cooldownMs;
    this.rotateMs = options.rotateMs;
    this.onData = options.onData;
    this.createDecoderFn =
      options.createDecoder ??
      (() => new prism.opus.Decoder({ frameSize: 960, channels: 2, rate: 48_000 }));
  }

  rotateIfNeeded(): void {
    if (!this.decoder || this.rotateMs <= 0) return;
    if (Date.now() - this.createdAt < this.rotateMs) return;
    this.destroy();
    this.ensureDecoder();
  }

  write(chunk: Buffer): void {
    const decoder = this.ensureDecoder();
    if (!decoder) return;
    try {
      decoder.write(chunk);
    } catch (error) {
      console.warn("[recorder] Opus decoder write failed, cooling down:", error);
      this.coolDown();
    }
  }

  destroy(): void {
    if (!this.decoder) return;
    this.decoder.removeAllListeners();
    this.decoder.destroy();
    this.decoder = null;
    this.createdAt = 0;
  }

  private ensureDecoder(): prism.opus.Decoder | null {
    if (this.decoder) return this.decoder;
    if (Date.now() < this.disabledUntil) return null;
    try {
      const decoder = this.createDecoderFn();
      decoder.on("data", this.onData);
      decoder.on("error", (error) => {
        console.warn("[recorder] Opus decoder error, cooling down:", error);
        this.coolDown();
      });
      this.decoder = decoder;
      this.createdAt = Date.now();
      return decoder;
    } catch (error) {
      console.warn("[recorder] Opus decoder init failed, cooling down:", error);
      this.disabledUntil = Date.now() + this.cooldownMs;
      return null;
    }
  }

  private coolDown(): void {
    this.disabledUntil = Date.now() + this.cooldownMs;
    this.destroy();
  }
}
  • Step 4: Run tests and typecheck
bun run test tests/recorder/decoder.test.ts
bun run typecheck

Expected: PASS.

  • Step 5: Commit
git add src/recorder/decoder.ts tests/recorder/decoder.test.ts
git commit -m "refactor: extract opus decoder lifecycle"

Task 6: Segment tests and implementation

Files:

  • Create: src/recorder/segment.ts

  • Create: tests/recorder/segment.test.ts

  • Step 1: Write tests for pure filename and rotation decision helpers

Create tests/recorder/segment.test.ts:

import { describe, expect, it } from "vitest";
import { buildSegmentPaths, shouldRotateSegment } from "../../src/recorder/segment";

describe("buildSegmentPaths", () => {
  it("creates matching ogg and json paths", () => {
    expect(buildSegmentPaths("/tmp/user", 123)).toEqual({
      filename: "/tmp/user/123.ogg",
      jsonFilename: "/tmp/user/123.json",
    });
  });
});

describe("shouldRotateSegment", () => {
  it("rotates only when segment limit is exceeded", () => {
    expect(shouldRotateSegment(1_000, 1_499, 500)).toBe(false);
    expect(shouldRotateSegment(1_000, 1_500, 500)).toBe(true);
    expect(shouldRotateSegment(1_000, 2_000, 0)).toBe(false);
  });
});
  • Step 2: Run failing test
bun run test tests/recorder/segment.test.ts

Expected: FAIL because module does not exist.

  • Step 3: Implement segment helpers and manager

Create src/recorder/segment.ts:

import fs from "node:fs";
import path from "node:path";
import prism from "prism-media";
import type { SegmentState } from "../types";

export function buildSegmentPaths(userDir: string, startTime: number): { filename: string; jsonFilename: string } {
  return {
    filename: path.join(userDir, `${startTime}.ogg`),
    jsonFilename: path.join(userDir, `${startTime}.json`),
  };
}

export function shouldRotateSegment(startTime: number, now: number, recordingSegmentMs: number): boolean {
  return recordingSegmentMs > 0 && now - startTime >= recordingSegmentMs;
}

export class SegmentManager {
  private currentSegment: SegmentState | null = null;
  private segmentIndex = 0;

  constructor(
    private readonly userDir: string,
    private readonly recordingSegmentMs: number,
  ) {}

  open(oggPacketStream: NodeJS.ReadableStream): SegmentState {
    const index = this.segmentIndex++;
    const startTime = Date.now();
    const { filename, jsonFilename } = buildSegmentPaths(this.userDir, startTime);
    const oggStream = new prism.opus.OggLogicalBitstream({
      opusHead: new prism.opus.OpusHead({ channelCount: 2, sampleRate: 48_000 }),
      pageSizeControl: { maxPackets: 10 },
      crc: true,
    });
    const out = fs.createWriteStream(filename);
    oggPacketStream.pipe(oggStream).pipe(out);

    this.currentSegment = { index, startTime, endTime: null, filename, jsonFilename, oggStream, out };
    return this.currentSegment;
  }

  close(oggPacketStream: NodeJS.ReadableStream): SegmentState | null {
    if (!this.currentSegment) return null;
    const segment = this.currentSegment;
    segment.endTime = Date.now();
    oggPacketStream.unpipe(segment.oggStream);
    segment.oggStream.end();
    this.currentSegment = null;
    return segment;
  }

  rotateIfNeeded(oggPacketStream: NodeJS.ReadableStream): SegmentState | null {
    if (!this.currentSegment) return null;
    if (!shouldRotateSegment(this.currentSegment.startTime, Date.now(), this.recordingSegmentMs)) return null;
    this.close(oggPacketStream);
    return this.open(oggPacketStream);
  }

  getCurrent(): SegmentState | null {
    return this.currentSegment;
  }
}
  • Step 4: Run tests and typecheck
bun run test tests/recorder/segment.test.ts
bun run typecheck

Expected: PASS.

  • Step 5: Commit
git add src/recorder/segment.ts tests/recorder/segment.test.ts
git commit -m "refactor: extract recording segment manager"

Task 7: Audio stream helper

Files:

  • Create: src/recorder/audioStream.ts

  • Step 1: Create stream helper

Create src/recorder/audioStream.ts:

import { EndBehaviorType, type VoiceReceiver } from "@discordjs/voice";

export interface AudioStreamHandlers {
  onPacket: (chunk: Buffer) => void;
  onEnd: () => void;
  onError: (error: Error) => void;
}

export function subscribeToAudioStream(
  receiver: VoiceReceiver,
  userId: string,
  handlers: AudioStreamHandlers,
): NodeJS.ReadableStream {
  const audioStream = receiver.subscribe(userId, {
    end: {
      behavior: EndBehaviorType.AfterSilence,
      duration: 3_000,
    },
  });

  audioStream.on("data", handlers.onPacket);
  audioStream.on("end", handlers.onEnd);
  audioStream.on("error", handlers.onError);

  return audioStream;
}
  • Step 2: Run typecheck
bun run typecheck

Expected: PASS.

  • Step 3: Commit
git add src/recorder/audioStream.ts
git commit -m "refactor: extract audio stream subscription"

Task 8: Refactor src/recorder.ts orchestration

Files:

  • Modify: src/recorder.ts

  • Step 1: Replace inline helpers with modules

Edit src/recorder.ts:

  • Import collectUserMetadata, createSegmentMetadata.
  • Import OpusDecoder.
  • Import SegmentManager.
  • Import subscribeToAudioStream.
  • Use config.recordingsDir, config.recordingSegmentMs, config.decoderRotateMs, config.decoderCooldownMs.
  • Keep startRecording(client, channel) and stopRecording(guildId) exports unchanged.
  • Remove packet debug logging Pkt #....
  • Keep current global web update behavior via globalThis as PcmBroadcaster.

Core packet handler shape:

const broadcaster = globalThis as typeof globalThis & PcmBroadcaster;
const userMetadata = await collectUserMetadata(client, userId, channel);
const segmentManager = new SegmentManager(userDir, config.recordingSegmentMs);
const decoder = new OpusDecoder({
  cooldownMs: config.decoderCooldownMs,
  rotateMs: config.decoderRotateMs,
  onData: (pcm) => {
    if (!broadcaster.broadcastPcmToWeb) return;
    const outBuf = Buffer.alloc(pcm.length / 4);
    for (let i = 0; i < outBuf.length / 2; i++) {
      outBuf.writeInt16LE(pcm.readInt16LE(i * 8), i * 2);
    }
    broadcaster.broadcastPcmToWeb(outBuf, userId);
  },
});

const audioStream = subscribeToAudioStream(receiver, userId, {
  onPacket: (chunk) => {
    if (chunk.length < 8) return;
    segmentManager.rotateIfNeeded(oggPacketStream);
    if (!broadcaster.broadcastPcmToWeb) return;
    decoder.rotateIfNeeded();
    decoder.write(chunk);
  },
  onEnd: () => {
    const segment = segmentManager.close(oggPacketStream);
    decoder.destroy();
    if (segment) {
      const metadata = createSegmentMetadata(userMetadata, segment, sessionId, sessionStartTime, config.recordingSegmentMs);
      fs.writeFileSync(segment.jsonFilename, JSON.stringify(metadata, null, 2));
    }
    broadcaster.updateActiveUser?.(userId, { username: userMetadata.username, avatar: userMetadata.avatarUrl, speaking: false });
  },
  onError: (error) => {
    segmentManager.close(oggPacketStream);
    decoder.destroy();
    console.error(`[recorder] Audio Stream error ${userId}:`, error.message);
  },
});
  • Step 2: Preserve metadata writes on segment finish

If existing behavior writes JSON when out finishes, attach out.on("finish", ...) in SegmentManager.open() caller after opening current segment. Ensure every closed segment gets JSON metadata, including rotated segments.

  • Step 3: Run typecheck
bun run typecheck

Expected: PASS. Fix type errors by narrowing types, not adding any unless third-party library lacks exported type.

  • Step 4: Run tests
bun run test

Expected: PASS.

  • Step 5: Commit
git add src/recorder.ts src/recorder/audioStream.ts src/recorder/decoder.ts src/recorder/metadata.ts src/recorder/segment.ts src/types.ts
git commit -m "refactor: modularize recorder orchestration"

Task 9: Format and lint cleanup

Files:

  • Modify: all formatted TypeScript/config files touched by Biome.

  • Step 1: Run formatter

bun run format

Expected: files formatted.

  • Step 2: Run linter
bun run lint

Expected: PASS or actionable warnings. Fix warnings that are in touched code.

  • Step 3: Run typecheck and tests
bun run typecheck
bun run test

Expected: PASS.

  • Step 4: Commit
git add .
git commit -m "style: format and lint codebase"

Task 10: Manual runtime verification

Files:

  • No required code changes unless verification finds bug.

  • Step 1: Start app

Run:

bun run start

Expected: app starts, logs bot ready or fails only due missing env credentials.

  • Step 2: If env exists, verify recording flow

Manual steps:

  1. Join configured Discord voice channel.
  2. Speak for >3 seconds.
  3. Confirm .ogg file and .json metadata are created under RECORDINGS_DIR.
  4. Keep speaking past RECORDING_SEGMENT_MS; confirm segment rotation creates multiple files.
  5. Stop app with Ctrl-C; confirm graceful shutdown log.
  • Step 3: Commit fixes if needed
git add src tests package.json biome.json vitest.config.ts tsconfig.json
git commit -m "fix: preserve recorder runtime behavior"

Only run if code changed.


Task 11: Final verification

Files:

  • No changes expected.

  • Step 1: Run full verification

bun run format
bun run lint
bun run typecheck
bun run test
git status --short

Expected: formatter stable, lint PASS, typecheck PASS, tests PASS, git status clean or only intentional uncommitted runtime artifacts excluded by .gitignore.

  • Step 2: Review diff summary
git log --oneline -8
git diff HEAD~8...HEAD --stat

Expected: commits show tooling, config, types, metadata, decoder, segment, stream, recorder refactor, formatting.

  • Step 3: Report result

Report:

  • Commands run and pass/fail status.
  • Runtime verification status.
  • Any remaining risks, especially Discord runtime behavior if not manually tested with credentials.

Self-Review

  • Spec coverage: tooling, config, shared types, metadata, decoder, segment, audio stream, recorder orchestration, tests, lint/format, and runtime verification are covered.
  • Placeholder scan: no TBD/TODO placeholders.
  • Type consistency: UserMetadata, SegmentState, SegmentMetadata, PcmBroadcaster, OpusDecoder, SegmentManager, and subscribeToAudioStream names are consistent across tasks.