Files
OwnCord/Server/cmd/seed/profile_alpha.go
T
J3vbandClaude bd397d2bbc feat(b3-7): alpha-shaped dataset — deterministic seed profile and the v1.2.0-alpha.4 snapshot (#1469)
go run ./cmd/seed -confirm-dev -profile alpha fills an empty database with
the plan's dataset: 100 users (1/2/5/92 across the four roles), 12 channels
(10 text + 2 voice; 3 role-override, 2 user-override, 1 archived), 20,000
messages over 30 simulated days on a diurnal curve (exactly 15% in DMs
across 40 pairs), 300 attachment rows (60/10/10/20%, 10KB-5MB), 500
reactions, 30 invites (10 revoked), one disabled plugin row. Deterministic
by construction — fixed seed, fixed clock, constant bcrypt hash, explicit
ids and timestamps, VACUUM INTO as the canonical bytes — and
TestAlphaProfileByteIdentical holds the property (two full runs compared
byte for byte; a schema_versions wall-clock leak was the one leak found,
now pinned by the scrub). Two constants deliberately leave no rows and say
why in the package comment: voice sessions are LiveKit-ephemeral, and the
replay log is empty exactly as on a server restarted for an upgrade.

The committed snapshot (3.2MB, under the 5MB LFS line) is the scrubbed
VACUUM of that profile at the alpha.4 migration set - the schema has not
moved since the tag, so it is a true alpha.4 artifact. scrub.sql beside it
also anonymises a real donated database. db/alpha_snapshot_test.go is the
standing canary: provenance (31 applied migrations), HEAD migrations apply
cleanly, and every promised row count checks out, FTS included. Consumers
(B4 HP-4, B6 upgrade rehearsal, B10 in-place upgrade) are named in the
snapshot README and docs/deployment.md.


Claude-Session: https://claude.ai/code/session_01B8dwVLEihnGZYtH9X631F4

Co-authored-by: Claude <noreply@anthropic.com>
2026-08-31 06:12:09 +00:00

770 lines
28 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// The alpha-shaped profile (B3-7, roadmap workstream 12).
//
// `go run ./cmd/seed -confirm-dev -profile alpha` fills an EMPTY database with
// a deterministic dataset shaped like a real 1.2.0-alpha.4 server after a
// month of use. The same profile, scrubbed and vacuumed, is the committed
// snapshot at Server/testdata/snapshots/v1.2.0-alpha.4.sqlite that B4's HP-4
// drills, B6's upgrade rehearsal and B10's in-place upgrade consume — the
// schema has not changed since the v1.2.0-alpha.4 tag (zero migrations added),
// so a database seeded on today's migrations IS an alpha.4-schema database.
//
// Determinism is the contract: fixed rng seed, fixed clock, a constant bcrypt
// hash (live hashing salts every run), explicit ids and timestamps on every
// row, one insert order, and `VACUUM INTO` as the byte-canonical output.
// TestAlphaProfileByteIdentical holds it: two runs, two files, bytes.Equal.
//
// The numbers below are the plan's (b3-server-architecture-guardrails
// §B3-7). A number found unrepresentative is changed HERE with the reason in
// the B3-7 evidence block — never silently. Two constants deliberately leave
// no rows, recorded rather than hidden:
//
// - alphaVoiceSessions: voice is LiveKit-ephemeral. A finished session
// leaves no at-rest row in this schema (voice_states is live presence and
// empties on disconnect), so a cold snapshot of a quiescent server
// carries none. The constant stays because the load harness and any
// future voice-history feature consume the same shape.
// - events: the replay log is empty, as on any server restarted for an
// upgrade — exactly the state the snapshot's consumers rehearse.
// Fabricated frames would be replayed verbatim to real clients.
//
// The seeded accounts all use the password "alpha-dev-password" (constant
// hash below), which is why -confirm-dev stays mandatory.
package main
import (
"context"
"database/sql"
"fmt"
"log"
"math/rand"
"os"
"sort"
"strings"
"time"
"github.com/J3vb/OwnCord/Server/db"
"github.com/J3vb/OwnCord/Server/permissions"
)
// ─── The profile, as constants ──────────────────────────────────────────────
const (
alphaSeed = 20260831 // fixed rng seed; changing it is changing the dataset
alphaUsers = 100 // 1 owner + 2 admins + 5 moderators + 92 members
alphaOwners = 1
alphaAdmins = 2
alphaModerators = 5
alphaChannels = 12 // 10 text + 2 voice; 3 role-override, 2 user-override, 1 archived
alphaMessages = 20000
alphaWindowDays = 30
alphaDMShare = 0.15 // exactly 3,000 of the 20,000 land in DMs
alphaDMPairs = 40
alphaAttachments = 300 // image/audio/video/other 60/10/10/20 %
alphaReactions = 500
alphaInvites = 30 // 10 of them revoked
alphaInvitesRevkd = 10
alphaPluginRows = 1 // disabled
// alphaVoiceSessions is a shape parameter with no at-rest rows — see the
// package comment. 145 min, 26 participants when a live harness uses it.
alphaVoiceSessions = 200
// alphaPasswordHash is bcrypt("alpha-dev-password"), fixed so two seed
// runs produce identical bytes. Weak and public by design: -confirm-dev.
//nolint:gosec // G101: a deliberately public dev-profile hash ("alpha-dev-password"); -confirm-dev gates every use
alphaPasswordHash = "$2a$12$EBrRXmplT1ryU0o/HzELSePreo.gK5.z5Tjo4ec/ISchy5gKwxtQq"
)
// alphaWindowEnd is the fixed clock: the last instant of the simulated month.
var alphaWindowEnd = time.Date(2026, 8, 1, 0, 0, 0, 0, time.UTC)
// alphaHourWeights shapes the diurnal curve — quiet nights, an evening peak.
// Relative weights per UTC hour; largest-remainder allocation makes the
// per-bucket counts sum exactly to the day's total.
var alphaHourWeights = [24]int{
1, 1, 1, 1, 1, 2, // 0005
3, 5, 7, 8, 8, 8, // 0611
9, 9, 8, 8, 9, 10, // 1217
11, 12, 11, 8, 5, 2, // 1823
}
// alphaLexicon feeds the message generator. Plain words only: the content is
// synthetic on purpose and never pretends to be a real person's text.
var alphaLexicon = []string{
"the", "a", "we", "you", "it", "that", "this", "just", "really", "pretty",
"server", "channel", "voice", "call", "message", "update", "build", "bug",
"fix", "release", "game", "night", "stream", "clip", "map", "round",
"team", "later", "today", "tomorrow", "tonight", "morning", "again",
"works", "broke", "fixed", "joined", "left", "restarted", "uploaded",
"good", "great", "weird", "slow", "fast", "new", "old", "same",
"anyone", "someone", "everyone", "nobody", "who", "what", "when", "why",
"up", "down", "in", "out", "on", "off", "for", "with", "about", "after",
"lol", "nice", "thanks", "ok", "sure", "maybe", "yes", "no", "brb", "gg",
}
// ─── Channel definitions ────────────────────────────────────────────────────
type alphaChannel struct {
name, ctype, category, topic string
position int
archived bool
}
// Ten text and two voice channels. Indices are ids-1; the overrides below
// refer to them by id. Channel 10 is the archived one and only carries
// early-window history.
var alphaChannelDefs = []alphaChannel{
{"welcome", "text", "Information", "Start here — rules and invites", 0, false},
{"announcements", "text", "Information", "Server news; staff post, everyone reads", 1, false},
{"general", "text", "Text Channels", "General chat", 2, false},
{"random", "text", "Text Channels", "Off-topic", 3, false},
{"gaming", "text", "Text Channels", "What are we playing", 4, false},
{"media", "text", "Text Channels", "Clips, screenshots, links", 5, false},
{"dev", "text", "Text Channels", "Server tinkering and self-hosting", 6, false},
{"support", "text", "Text Channels", "Ask for help here", 7, false},
{"staff", "text", "Staff", "Staff-only coordination", 8, false},
{"archive-2026-06", "text", "Archive", "June history, read-only", 9, true},
{"Lounge", "voice", "Voice", "", 10, false},
{"Game Night", "voice", "Voice", "", 11, false},
}
// ─── Entry point ────────────────────────────────────────────────────────────
// runAlpha is the -profile alpha path: migrate, seed, optionally scrub and
// write the byte-canonical snapshot.
func runAlpha(database *db.DB, snapshotPath, scrubPath string) int {
if err := db.Migrate(database); err != nil {
log.Printf("failed to run migrations: %v", err)
return 1
}
if err := seedAlpha(database); err != nil {
log.Printf("%v", err)
return 1
}
fmt.Println("--- Alpha profile seeded ---")
fmt.Printf(" Users: %d\n", alphaUsers)
fmt.Printf(" Channels: %d (+%d DM channels)\n", alphaChannels, alphaDMPairs)
fmt.Printf(" Messages: %d over %d days (%.0f%% in DMs)\n", alphaMessages, alphaWindowDays, alphaDMShare*100)
fmt.Printf(" Attachments: %d · Reactions: %d · Invites: %d (%d revoked) · Plugins: %d\n",
alphaAttachments, alphaReactions, alphaInvites, alphaInvitesRevkd, alphaPluginRows)
fmt.Printf(" Voice sessions: %d — a shape parameter; a finished session leaves no at-rest row (package comment)\n", alphaVoiceSessions)
fmt.Println(" Password for every account: alpha-dev-password")
if snapshotPath != "" {
if scrubPath == "" {
log.Printf("-snapshot requires -scrub (the committed scrub script)")
return 1
}
if err := writeSnapshot(database, scrubPath, snapshotPath); err != nil {
log.Printf("%v", err)
return 1
}
fmt.Printf(" Snapshot: %s\n", snapshotPath)
}
return 0
}
// seedAlpha fills an empty database with the profile. It refuses a non-empty
// one: determinism is per-database-lifetime, and "empty" is the only state
// two runs can share.
func seedAlpha(database *db.DB) error {
var users int
if err := database.QueryRowContext(context.Background(),
`SELECT COUNT(*) FROM users`).Scan(&users); err != nil {
return fmt.Errorf("alpha: counting users: %w", err)
}
if users > 0 {
return fmt.Errorf("alpha: the profile seeds only an empty database (found %d users); point -db at a fresh file", users)
}
//nolint:gosec // G404: byte-for-byte determinism is this profile's contract; nothing here is cryptographic
rng := rand.New(rand.NewSource(alphaSeed))
windowStart := alphaWindowEnd.AddDate(0, 0, -alphaWindowDays)
joined := alphaJoinTimes(rng, windowStart)
tx, err := database.BeginTx(context.Background(), &sql.TxOptions{})
if err != nil {
return fmt.Errorf("alpha: begin: %w", err)
}
defer func() { _ = tx.Rollback() }()
if err := alphaInsertUsers(tx, rng, joined); err != nil {
return err
}
if err := alphaInsertChannels(tx, windowStart); err != nil {
return err
}
pairs, err := alphaInsertDMs(tx, rng, joined)
if err != nil {
return err
}
messageTimes, messageAuthors, err := alphaInsertMessages(tx, rng, windowStart, joined, pairs)
if err != nil {
return err
}
if err := alphaInsertAttachments(tx, rng, messageTimes, messageAuthors); err != nil {
return err
}
if err := alphaInsertReactions(tx, rng); err != nil {
return err
}
if err := alphaInsertInvites(tx, rng, windowStart); err != nil {
return err
}
if _, err := tx.Exec(
`INSERT INTO plugins (id, name, version, enabled, manifest_json, installed_at) VALUES (1, ?, ?, 0, ?, ?)`,
"example-echo", "0.1.0",
`{"name":"example-echo","version":"0.1.0","description":"seed profile placeholder; never enabled"}`,
ts(windowStart.Add(24*time.Hour)),
); err != nil {
return fmt.Errorf("alpha: plugin row: %w", err)
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("alpha: commit: %w", err)
}
return nil
}
// writeSnapshot applies the scrub script and writes the byte-canonical
// snapshot via VACUUM INTO (which refuses to overwrite; remove a stale
// target first so regeneration is one command).
func writeSnapshot(database *db.DB, scrubPath, outPath string) error {
scrub, err := os.ReadFile(scrubPath)
if err != nil {
return fmt.Errorf("alpha: reading scrub script: %w", err)
}
// Strip comment lines first, then split on ";" — a ";" inside a comment
// must not end a statement. The scrub script holds no string literal
// containing ";", which is the simplicity this parser is allowed.
var sqlOnly []string
for line := range strings.SplitSeq(string(scrub), "\n") {
if strings.HasPrefix(strings.TrimSpace(line), "--") {
continue
}
sqlOnly = append(sqlOnly, line)
}
for stmt := range strings.SplitSeq(strings.Join(sqlOnly, "\n"), ";") {
stmt = strings.TrimSpace(stmt)
if stmt == "" {
continue
}
if _, err := database.ExecContext(context.Background(), stmt); err != nil {
return fmt.Errorf("alpha: scrub statement %q: %w", stmt[:min(40, len(stmt))], err)
}
}
if err := os.Remove(outPath); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("alpha: removing stale snapshot: %w", err)
}
quoted := strings.ReplaceAll(outPath, "'", "''")
if _, err := database.ExecContext(context.Background(), "VACUUM INTO '"+quoted+"'"); err != nil {
return fmt.Errorf("alpha: VACUUM INTO %s: %w", outPath, err)
}
return nil
}
// ─── Users ──────────────────────────────────────────────────────────────────
// alphaJoinTimes computes every user's created_at once — the message and DM
// generators consult it so nothing is authored before its author joined (a
// fixture with time-travelling rows would poison the retention and
// account-age rehearsals that consume it; Codex on #1469, P2). Monotonic in
// the user id — the first forty accounts predate the window (an established
// server; all staff are among them), the remaining sixty join during it in
// id order — so "who exists at time t" is always the prefix 1..N.
func alphaJoinTimes(rng *rand.Rand, windowStart time.Time) []time.Time {
joined := make([]time.Time, alphaUsers+1)
for i := 1; i <= alphaUsers; i++ {
if i <= 40 {
joined[i] = windowStart.AddDate(0, 0, -60).Add(time.Duration(i) * 26 * time.Hour)
continue
}
// Spacing is 12h, jitter under 1h: monotonicity holds by construction.
into := time.Duration(i-41) * (time.Duration(alphaWindowDays) * 24 * time.Hour) / 60
joined[i] = windowStart.Add(into).Add(time.Duration(rng.Intn(3599)) * time.Second)
}
return joined
}
func alphaInsertUsers(tx *sql.Tx, rng *rand.Rand, joined []time.Time) error {
roleOf := func(i int) int { // i is 1-based user number
switch {
case i <= alphaOwners:
return 1
case i <= alphaOwners+alphaAdmins:
return 2
case i <= alphaOwners+alphaAdmins+alphaModerators:
return 3
default:
return 4
}
}
const cols = 7
var b strings.Builder
args := make([]any, 0, alphaUsers*cols)
for i := 1; i <= alphaUsers; i++ {
if i > 1 {
b.WriteString(",")
}
b.WriteString("(?,?,?,?,?,?,?)")
created := joined[i]
lastSeen := alphaWindowEnd.Add(-time.Duration(rng.Intn(72*3600)) * time.Second)
if lastSeen.Before(created) {
// A user who joined in the window's final days was last seen at
// the join, not before it.
lastSeen = created
}
args = append(args,
i, fmt.Sprintf("user%03d", i), alphaPasswordHash, roleOf(i),
"offline", ts(created), ts(lastSeen),
)
}
_, err := tx.Exec(
//nolint:gosec // G202: the concatenation is a builder of "(?,…)" placeholder groups; every value is bound
`INSERT INTO users (id, username, password, role_id, status, created_at, last_seen) VALUES `+b.String(),
args...,
)
if err != nil {
return fmt.Errorf("alpha: users: %w", err)
}
return nil
}
// ─── Channels and overrides ─────────────────────────────────────────────────
func alphaInsertChannels(tx *sql.Tx, windowStart time.Time) error {
created := windowStart.AddDate(0, 0, -60)
for i, c := range alphaChannelDefs {
archived := 0
if c.archived {
archived = 1
}
if _, err := tx.Exec(
`INSERT INTO channels (id, name, type, category, topic, position, archived, is_group, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?)`,
i+1, c.name, c.ctype, c.category, c.topic, c.position, archived, ts(created.Add(time.Duration(i)*time.Minute)),
); err != nil {
return fmt.Errorf("alpha: channel %q: %w", c.name, err)
}
}
// Three channels carry role overrides (ids 1, 2, 9), two carry user
// overrides (ids 9 and 4) — the plan's 3/2 split, with the layered case
// (channel 9 has both) included deliberately.
roleOverrides := []struct {
channel, role int64
allow, deny int64
}{
{1, 4, 0, permissions.SendMessages}, // #welcome: members read-only
{2, 4, 0, permissions.SendMessages}, // #announcements: members read-only
{2, 3, permissions.MentionEveryone, 0}, // …and moderators may @everyone there
{9, 4, 0, permissions.ReadMessages | permissions.SendMessages}, // #staff hidden from members
}
for _, o := range roleOverrides {
if _, err := tx.Exec(
`INSERT INTO channel_overrides (channel_id, role_id, allow, deny) VALUES (?, ?, ?, ?)`,
o.channel, o.role, o.allow, o.deny,
); err != nil {
return fmt.Errorf("alpha: role override ch=%d role=%d: %w", o.channel, o.role, err)
}
}
userOverrides := []struct {
channel, user int64
allow, deny int64
}{
{9, 9, permissions.ReadMessages | permissions.SendMessages, 0}, // a trusted member sees #staff
{4, 42, 0, permissions.SendMessages}, // user042 is channel-muted in #random
}
for _, o := range userOverrides {
if _, err := tx.Exec(
`INSERT INTO channel_user_overrides (channel_id, user_id, allow, deny) VALUES (?, ?, ?, ?)`,
o.channel, o.user, o.allow, o.deny,
); err != nil {
return fmt.Errorf("alpha: user override ch=%d user=%d: %w", o.channel, o.user, err)
}
}
return nil
}
// ─── DMs ────────────────────────────────────────────────────────────────────
type dmPair struct {
channelID int64
a, b int64
// ready is when both members exist — no message in the pair may precede
// it, and it is the channel's created_at / opened_at, matching
// GetOrCreateDMChannel's create-on-first-contact shape.
ready time.Time
}
// alphaInsertDMs creates the 40 DM channels exactly as GetOrCreateDMChannel
// would (type 'dm', empty name, both participants, both open). The first
// twelve pairs are drawn from the pre-window accounts so DM traffic exists
// from the window's first hour; the rest may involve joiners and only carry
// messages once both members exist.
func alphaInsertDMs(tx *sql.Tx, rng *rand.Rand, joined []time.Time) ([]dmPair, error) {
seen := map[[2]int64]bool{}
pairs := make([]dmPair, 0, alphaDMPairs)
for len(pairs) < alphaDMPairs {
pool := alphaUsers
if len(pairs) < 12 {
pool = 40 // both members pre-window
}
a := int64(rng.Intn(pool) + 1)
b := int64(rng.Intn(pool) + 1)
if a == b {
continue
}
if a > b {
a, b = b, a
}
if seen[[2]int64{a, b}] {
continue
}
seen[[2]int64{a, b}] = true
id := int64(alphaChannels + len(pairs) + 1) // 13..52
ready := joined[a]
if joined[b].After(ready) {
ready = joined[b]
}
if _, err := tx.Exec(
`INSERT INTO channels (id, name, type, is_group, created_at) VALUES (?, '', 'dm', 0, ?)`,
id, ts(ready),
); err != nil {
return nil, fmt.Errorf("alpha: dm channel %d: %w", id, err)
}
for _, u := range []int64{a, b} {
if _, err := tx.Exec(
`INSERT INTO dm_participants (channel_id, user_id) VALUES (?, ?)`, id, u,
); err != nil {
return nil, fmt.Errorf("alpha: dm participant: %w", err)
}
if _, err := tx.Exec(
`INSERT INTO dm_open_state (user_id, channel_id, opened_at) VALUES (?, ?, ?)`, u, id, ts(ready),
); err != nil {
return nil, fmt.Errorf("alpha: dm open state: %w", err)
}
}
pairs = append(pairs, dmPair{channelID: id, a: a, b: b, ready: ready})
}
return pairs, nil
}
// ─── Messages ───────────────────────────────────────────────────────────────
// alphaChannelWeights is the relative traffic per text channel (ids 1..10):
// #announcements and #welcome are quiet, #general and #gaming busy, #staff
// small, the archive early-only.
var alphaChannelWeights = []int{2, 1, 30, 18, 22, 10, 8, 6, 3, 8}
// alphaChannelFor picks a text channel for one message on the given day; the
// archived channel's history ends on day 10.
func alphaChannelFor(rng *rand.Rand, day int) int64 {
w := make([]int, len(alphaChannelWeights))
copy(w, alphaChannelWeights)
if day >= 10 {
w[9] = 0
}
total := 0
for _, x := range w {
total += x
}
n := rng.Intn(total)
for i, x := range w {
if n < x {
return int64(i + 1)
}
}
return 3
}
// alphaAuthorFor picks a message author. Staff channels (#welcome,
// #announcements) are staff-posted; #staff adds the override-listed user009;
// everywhere else, anyone who has joined by the message's time — maxUser is
// the eligibility prefix (staff and user009 are all pre-window).
func alphaAuthorFor(rng *rand.Rand, ch int64, maxUser int) int64 {
switch ch {
case 1, 2:
return int64(rng.Intn(alphaOwners+alphaAdmins+alphaModerators) + 1)
case 9:
staff := []int64{1, 2, 3, 4, 5, 6, 7, 8, 9}
return staff[rng.Intn(len(staff))]
default:
return int64(rng.Intn(maxUser) + 1)
}
}
// sqlBatch accumulates multi-row VALUES groups and flushes them in chunks —
// one statement per chunk instead of one per row, inside the caller's
// transaction.
type sqlBatch struct {
b strings.Builder
args []any
}
func (sb *sqlBatch) add(row string, vals ...any) {
if len(sb.args) > 0 {
sb.b.WriteString(",")
}
sb.b.WriteString(row)
sb.args = append(sb.args, vals...)
}
func (sb *sqlBatch) flush(tx *sql.Tx, prefix string) error {
if sb.b.Len() == 0 {
return nil
}
//nolint:gosec // G202: the concatenation is a builder of "(?,…)" placeholder groups; every value is bound
_, err := tx.Exec(prefix+sb.b.String(), sb.args...)
sb.b.Reset()
sb.args = sb.args[:0]
return err
}
// alphaInsertMessages writes all 20,000 messages in chronological order —
// per-bucket second offsets are sorted before ids are assigned, so ascending
// ids follow ascending timestamps exactly as insertion order does on a live
// server (Codex on #1469, P2) — and returns each message's timestamp and
// author (indexed by id-1) for the attachment rows. Authors are drawn only
// from users who exist at the message's time: the eligible set is always the
// prefix 1..maxUser because alphaJoinTimes is monotonic, and a DM pair
// carries traffic only from its ready time.
func alphaInsertMessages(tx *sql.Tx, rng *rand.Rand, windowStart time.Time, joined []time.Time, pairs []dmPair) ([]time.Time, []int64, error) {
// Exactly 15% of message ids are DM messages.
dmCount := int(float64(alphaMessages) * alphaDMShare)
isDM := make([]bool, alphaMessages)
for _, idx := range rng.Perm(alphaMessages)[:dmCount] {
isDM[idx] = true
}
perDay := largestRemainder(onesSlice(alphaWindowDays), alphaMessages)
// Pairs sorted by readiness, so the eligible set at any time is a prefix;
// skewedIndex over that prefix keeps the earliest (all-pre-window) pairs
// the chattiest, which is also the realistic shape.
byReady := make([]dmPair, len(pairs))
copy(byReady, pairs)
sort.Slice(byReady, func(i, j int) bool { return byReady[i].ready.Before(byReady[j].ready) })
times := make([]time.Time, 0, alphaMessages)
authors := make([]int64, 0, alphaMessages)
maxUser := 40
readyPairs := 0
const chunkArgs = 500 * 5
var batch sqlBatch
const insertPrefix = `INSERT INTO messages (id, channel_id, user_id, content, timestamp) VALUES `
id := 0
for day := range alphaWindowDays {
perHour := largestRemainder(alphaHourWeights[:], perDay[day])
for hour := range 24 {
offsets := make([]int, perHour[hour])
for k := range offsets {
offsets[k] = rng.Intn(3600)
}
sort.Ints(offsets)
base := windowStart.AddDate(0, 0, day).Add(time.Duration(hour) * time.Hour)
for _, off := range offsets {
at := base.Add(time.Duration(off) * time.Second)
for maxUser < alphaUsers && !joined[maxUser+1].After(at) {
maxUser++
}
for readyPairs < len(byReady) && !byReady[readyPairs].ready.After(at) {
readyPairs++
}
var ch, author int64
if isDM[id] && readyPairs > 0 {
p := byReady[skewedIndex(rng, readyPairs)]
ch = p.channelID
if rng.Intn(2) == 0 {
author = p.a
} else {
author = p.b
}
} else {
ch = alphaChannelFor(rng, day)
author = alphaAuthorFor(rng, ch, maxUser)
}
id++
times = append(times, at)
authors = append(authors, author)
batch.add("(?,?,?,?,?)", id, ch, author, sentence(rng), ts(at))
if len(batch.args) >= chunkArgs {
if err := batch.flush(tx, insertPrefix); err != nil {
return nil, nil, fmt.Errorf("alpha: messages: %w", err)
}
}
}
}
}
if err := batch.flush(tx, insertPrefix); err != nil {
return nil, nil, fmt.Errorf("alpha: messages: %w", err)
}
return times, authors, nil
}
// ─── Attachments, reactions, invites ────────────────────────────────────────
func alphaInsertAttachments(tx *sql.Tx, rng *rand.Rand, messageTimes []time.Time, messageAuthors []int64) error {
type class struct {
count int
mime, ext string
minKB, maxKB int
namePrefix string
}
classes := []class{
{alphaAttachments * 60 / 100, "image/png", "png", 10, 900, "screenshot"},
{alphaAttachments * 10 / 100, "audio/ogg", "ogg", 50, 2000, "voice-note"},
{alphaAttachments * 10 / 100, "video/mp4", "mp4", 500, 5000, "clip"},
{alphaAttachments * 20 / 100, "application/pdf", "pdf", 10, 1500, "doc"},
}
n := 0
for _, c := range classes {
for k := 0; k < c.count; k++ {
n++
msgID := rng.Intn(alphaMessages) + 1
id := fmt.Sprintf("%08x%08x%08x%08x", rng.Uint32(), rng.Uint32(), rng.Uint32(), rng.Uint32())
size := (c.minKB + rng.Intn(c.maxKB-c.minKB+1)) * 1024
// The uploader is the message's author — CreateAttachment always
// records one on the current schema (Codex on #1469, P2).
if _, err := tx.Exec(
`INSERT INTO attachments (id, message_id, filename, stored_as, mime_type, size, uploaded_at, uploader_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
id, msgID,
fmt.Sprintf("%s-%03d.%s", c.namePrefix, n, c.ext),
id+"."+c.ext, c.mime, size, ts(messageTimes[msgID-1]), messageAuthors[msgID-1],
); err != nil {
return fmt.Errorf("alpha: attachment %d: %w", n, err)
}
}
}
return nil
}
func alphaInsertReactions(tx *sql.Tx, rng *rand.Rand) error {
emojis := []string{"👍", "😂", "🎉", "❤️", "🔥", "😮"}
type key struct {
m, u int64
e string
}
seen := map[key]bool{}
id := 0
for id < alphaReactions {
k := key{
m: int64(rng.Intn(alphaMessages) + 1),
u: int64(rng.Intn(alphaUsers) + 1),
e: emojis[rng.Intn(len(emojis))],
}
if seen[k] {
continue
}
seen[k] = true
id++
if _, err := tx.Exec(
`INSERT INTO reactions (id, message_id, user_id, emoji) VALUES (?, ?, ?, ?)`,
id, k.m, k.u, k.e,
); err != nil {
return fmt.Errorf("alpha: reaction %d: %w", id, err)
}
}
return nil
}
func alphaInsertInvites(tx *sql.Tx, rng *rand.Rand, windowStart time.Time) error {
staffCount := alphaOwners + alphaAdmins + alphaModerators
for i := 1; i <= alphaInvites; i++ {
revoked := 0
if i <= alphaInvitesRevkd {
revoked = 1
}
var maxUses any
if i%3 == 0 {
maxUses = 10
}
var expires any
if i%4 == 0 {
expires = ts(alphaWindowEnd.AddDate(0, 0, 7))
}
if _, err := tx.Exec(
`INSERT INTO invites (id, code, created_by, max_uses, use_count, expires_at, created_at, revoked)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
i, fmt.Sprintf("ALPHA-INV-%02d", i), rng.Intn(staffCount)+1,
maxUses, rng.Intn(8), expires,
ts(windowStart.Add(time.Duration(i)*13*time.Hour)), revoked,
); err != nil {
return fmt.Errorf("alpha: invite %d: %w", i, err)
}
}
return nil
}
// ─── Small helpers ──────────────────────────────────────────────────────────
// ts renders a time the way the schema's datetime('now') defaults do.
func ts(t time.Time) string { return t.UTC().Format("2006-01-02 15:04:05") }
// sentence builds 314 lexicon words; median chat-message length.
func sentence(rng *rand.Rand) string {
n := 3 + rng.Intn(12)
words := make([]string, n)
for i := range words {
words[i] = alphaLexicon[rng.Intn(len(alphaLexicon))]
}
return strings.Join(words, " ")
}
// skewedIndex prefers low indexes (min of two uniforms), so a few DM pairs
// are chatty and the tail is sparse — the shape real DM traffic has.
func skewedIndex(rng *rand.Rand, n int) int {
a, b := rng.Intn(n), rng.Intn(n)
if b < a {
return b
}
return a
}
// largestRemainder allocates total across weights, summing exactly to total.
func largestRemainder(weights []int, total int) []int {
sum := 0
for _, w := range weights {
sum += w
}
out := make([]int, len(weights))
type rem struct {
i int
frac float64
}
rems := make([]rem, len(weights))
assigned := 0
for i, w := range weights {
exact := float64(total) * float64(w) / float64(sum)
out[i] = int(exact)
assigned += out[i]
rems[i] = rem{i, exact - float64(out[i])}
}
// Stable selection sort by remainder — deterministic, n is tiny.
for assigned < total {
best := 0
for j := 1; j < len(rems); j++ {
if rems[j].frac > rems[best].frac {
best = j
}
}
out[rems[best].i]++
rems[best].frac = -1
assigned++
}
return out
}
func onesSlice(n int) []int {
s := make([]int, n)
for i := range s {
s[i] = 1
}
return s
}