# Fase 3.0 — Fondamenta dominio · 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:** Costruire lo strato dati + configurazione del motore eventi (schema Drizzle V1, config Zod + preset Bolgia, `neon-serverless` + migrazioni versionate, guardia ruoli) senza UI né algoritmi.

**Architecture:** Lo schema cresce in una cartella `db/schema/` (un file per area). La "formula" vive in `Phase.config` JSONB **tipizzato** da una `z.discriminatedUnion` su `kind`; i preset Bolgia sono costanti TS validate a load-time. Il client Drizzle passa a `neon-serverless` (transazioni) e si adottano migrazioni versionate con baseline pulita (DB a 0 dati). La sessione si carica una volta in `event.locals` con helper `requireRole`.

**Tech Stack:** SvelteKit (Svelte 5), Drizzle ORM 0.45 (`neon-serverless`), `@neondatabase/serverless` Pool, Zod, Better Auth 1.6.14, Neon Postgres, Vitest.

**Spec:** [`docs/superpowers/specs/2026-06-09-fase-3-0-fondamenta-design.md`](../specs/2026-06-09-fase-3-0-fondamenta-design.md)

---

## Note operative

- **Path con spazi**: usa sempre le virgolette e i flag `-C`/`--cwd`. Comandi `pnpm` dalla root `"/Users/paolo/Server/Siti Web/Lavoro/TtA"`.
- **Branch**: `fase-3-0-fondamenta` (già creato; lo spec è committato lì).
- **drizzle-kit bin reale**: `node node_modules/drizzle-kit/bin.cjs <cmd>` (lo shim `.bin/drizzle-kit` non gira con `node`).
- **Step DB (⚠️ controller)**: i passi che toccano Neon (migrate/baseline) o che lanciano test d'integrazione/E2E sono marcati **⚠️ DB** e li esegue il controller con autorizzazione; i subagent si fermano prima e li segnalano.
- **Gate offline ripetuti**: `pnpm check` (0/0), `pnpm test` (unit, offline), `pnpm build`.

## File Structure

**Create:**
- `src/lib/server/config/phase-config.ts` — Zod `discriminatedUnion` su `kind` + tipi (`PhaseConfig`, `GironiConfig`, `FormatConfig`).
- `src/lib/server/config/phase-config.test.ts` — unit (preset validi, config incoerenti rifiutate).
- `src/lib/server/config/templates.ts` — costanti `BOLGIA_13/7/4` + `FORMAT_TEMPLATES`.
- `src/lib/server/config/templates.test.ts` — unit (i preset parse-ano).
- `src/lib/server/db/schema/enums.ts` — `pgEnum` condivisi.
- `src/lib/server/db/schema/auth.ts` — user(+`ruolo`)/session/account/verification + relations (ex `auth-schema.ts`).
- `src/lib/server/db/schema/player.ts` — `player` + `playerAlias`.
- `src/lib/server/db/schema/events.ts` — `season`, `event`, `phase`, `girone`.
- `src/lib/server/db/schema/matches.ts` — `match`, `matchParticipant`, `standing`.
- `src/lib/server/db/schema/ranking.ts` — `ranking`, `sanction`.
- `src/lib/server/db/schema/misc.ts` — `eventRegistration`, `notification`, `content`.
- `src/lib/server/db/schema/index.ts` — re-export.
- `src/lib/server/authz.ts` — gerarchia ruoli + `requireRole`/`requireAdmin`.
- `src/lib/server/authz.test.ts` — unit.
- `src/lib/server/db/domain.integration.test.ts` — round-trip dominio (⚠️ DB).

**Modify:**
- `src/lib/server/db/index.ts` — switch a `neon-serverless` Pool.
- `src/lib/server/auth.ts` — `user.additionalFields.ruolo`.
- `src/hooks.server.ts` — popola `event.locals`.
- `src/app.d.ts` — tipi `App.Locals`.
- `src/routes/dashboard/+page.server.ts`, `src/routes/profilo/+page.server.ts` — usano `locals`.
- `drizzle.config.ts` — `schema` → cartella.
- `package.json` — `zod` dep + script `db:generate`/`db:migrate`.

**Delete:** `src/lib/server/db/schema.ts`, `src/lib/server/db/auth-schema.ts` (contenuto spostato in `schema/`).

---

## Task 1: Config Zod `PhaseConfig` (+ dipendenza zod)

**Files:**
- Create: `src/lib/server/config/phase-config.ts`
- Test: `src/lib/server/config/phase-config.test.ts`

- [ ] **Step 1: Installa zod**

Run: `pnpm -C "/Users/paolo/Server/Siti Web/Lavoro/TtA" add zod`
Expected: `zod` aggiunto a `dependencies`.

- [ ] **Step 2: Write the failing test**

Crea `src/lib/server/config/phase-config.test.ts`:

```ts
import { describe, it, expect } from 'vitest';
import { phaseConfigSchema } from './phase-config';

const gironiOk = {
  kind: 'GIRONI_ROUND_ROBIN' as const,
  dimensione: 13,
  incontriPerCoppia: 2,
  composizione: [{ n: 6, giocatori: 3 }, { n: 4, giocatori: 4 }],
  tabellaPunti: { '2': [4, 0], '3': [10, 4, 0], '4': [11, 6, 2, 0] }
};

describe('phaseConfigSchema', () => {
  it('accetta una config gironi coerente', () => {
    expect(phaseConfigSchema.safeParse(gironiOk).success).toBe(true);
  });
  it('accetta dimensione == max giocatori (gironi-4)', () => {
    const g4 = { ...gironiOk, dimensione: 4, composizione: [{ n: 2, giocatori: 4 }], tabellaPunti: { '4': [11, 6, 2, 0] } };
    expect(phaseConfigSchema.safeParse(g4).success).toBe(true);
  });
  it('rifiuta dimensione < max giocatori', () => {
    const bad = { ...gironiOk, dimensione: 3, composizione: [{ n: 1, giocatori: 4 }], tabellaPunti: { '4': [11, 6, 2, 0] } };
    expect(phaseConfigSchema.safeParse(bad).success).toBe(false);
  });
  it('rifiuta tabellaPunti con numero di voci sbagliato', () => {
    const bad = { ...gironiOk, tabellaPunti: { '2': [4, 0], '3': [10, 4], '4': [11, 6, 2, 0] } };
    expect(phaseConfigSchema.safeParse(bad).success).toBe(false);
  });
  it('rifiuta giocatori fuori da {2,3,4}', () => {
    const bad = { ...gironiOk, composizione: [{ n: 6, giocatori: 5 }] };
    expect(phaseConfigSchema.safeParse(bad).success).toBe(false);
  });
  it('accetta PARTITA_SINGOLA valida e rifiuta nGiocatori errato', () => {
    expect(phaseConfigSchema.safeParse({ kind: 'PARTITA_SINGOLA', nGiocatori: 2 }).success).toBe(true);
    expect(phaseConfigSchema.safeParse({ kind: 'PARTITA_SINGOLA', nGiocatori: 5 }).success).toBe(false);
  });
  it('instrada sul kind giusto (discriminated union)', () => {
    const r = phaseConfigSchema.safeParse(gironiOk);
    expect(r.success && r.data.kind).toBe('GIRONI_ROUND_ROBIN');
  });
});
```

- [ ] **Step 3: Run test to verify it fails**

Run: `pnpm -C "/Users/paolo/Server/Siti Web/Lavoro/TtA" test`
Expected: FAIL — modulo `./phase-config` inesistente.

- [ ] **Step 4: Write the implementation**

Crea `src/lib/server/config/phase-config.ts`:

```ts
import { z } from 'zod';

const giocatori = z.union([z.literal(2), z.literal(3), z.literal(4)]);

const gironiBase = z.object({
  kind: z.literal('GIRONI_ROUND_ROBIN'),
  dimensione: z.number().int().positive(),
  incontriPerCoppia: z.number().int().positive(),
  composizione: z.array(z.object({ n: z.number().int().positive(), giocatori })).min(1),
  tabellaPunti: z.object({
    '2': z.array(z.number()).optional(),
    '3': z.array(z.number()).optional(),
    '4': z.array(z.number()).optional()
  }),
  promoRetro: z
    .object({
      default: z.object({ promo: z.number().int(), retro: z.number().int() }),
      inverno: z.object({ promo: z.number().int().optional(), retro: z.number().int().optional() }).optional()
    })
    .optional(),
  coda: z.object({ multipliOttimali: z.array(z.number().int().positive()) }).optional()
});

const partitaSingolaBase = z.object({
  kind: z.literal('PARTITA_SINGOLA'),
  nGiocatori: giocatori
});

const bracketBase = z.object({ kind: z.literal('BRACKET_ELIMINAZIONE') }).passthrough();
const squadreBase = z.object({ kind: z.literal('A_SQUADRE') }).passthrough();

export const phaseConfigSchema = z
  .discriminatedUnion('kind', [gironiBase, partitaSingolaBase, bracketBase, squadreBase])
  .superRefine((cfg, ctx) => {
    if (cfg.kind !== 'GIRONI_ROUND_ROBIN') return;
    const usati = [...new Set(cfg.composizione.map((c) => c.giocatori))];
    const maxG = Math.max(...usati);
    if (cfg.dimensione < maxG) {
      ctx.addIssue({ code: 'custom', message: 'dimensione deve essere >= del max giocatori per partita' });
    }
    const punti = cfg.tabellaPunti as Record<string, number[] | undefined>;
    for (const g of usati) {
      const voci = punti[String(g)];
      if (!voci) {
        ctx.addIssue({ code: 'custom', message: `tabellaPunti manca la voce per ${g} giocatori` });
      } else if (voci.length !== g) {
        ctx.addIssue({ code: 'custom', message: `tabellaPunti['${g}'] deve avere ${g} punteggi` });
      }
    }
  });

export type PhaseConfig = z.infer<typeof phaseConfigSchema>;
export type GironiConfig = z.infer<typeof gironiBase>;
/** Event.formatConfig: template/default ereditabile (per ora = un PhaseConfig). Rivedibile. */
export type FormatConfig = PhaseConfig;
```

- [ ] **Step 5: Run test to verify it passes**

Run: `pnpm -C "/Users/paolo/Server/Siti Web/Lavoro/TtA" test`
Expected: PASS (tutti i casi phase-config verdi).

- [ ] **Step 6: Verifica typecheck**

Run: `pnpm -C "/Users/paolo/Server/Siti Web/Lavoro/TtA" check`
Expected: 0 errori.

- [ ] **Step 7: Commit**

```bash
git add package.json pnpm-lock.yaml src/lib/server/config/phase-config.ts src/lib/server/config/phase-config.test.ts
git commit -m "feat(fase3.0): config Zod PhaseConfig (discriminated union su kind) + zod"
```

---

## Task 2: FormatTemplate Bolgia 13/7/4

**Files:**
- Create: `src/lib/server/config/templates.ts`
- Test: `src/lib/server/config/templates.test.ts`

- [ ] **Step 1: Write the failing test**

Crea `src/lib/server/config/templates.test.ts`:

```ts
import { describe, it, expect } from 'vitest';
import { phaseConfigSchema } from './phase-config';
import { FORMAT_TEMPLATES, BOLGIA_13, BOLGIA_7, BOLGIA_4 } from './templates';

describe('FormatTemplates', () => {
  it('BOLGIA_13/7/4 sono config gironi valide', () => {
    for (const t of [BOLGIA_13, BOLGIA_7, BOLGIA_4]) {
      expect(phaseConfigSchema.safeParse(t).success).toBe(true);
    }
  });
  it('FORMAT_TEMPLATES contiene i 3 preset', () => {
    expect(Object.keys(FORMAT_TEMPLATES).sort()).toEqual(['BOLGIA_13', 'BOLGIA_4', 'BOLGIA_7']);
  });
  it('BOLGIA_13 ha dimensione 13 e composizione 6x3 + 4x4', () => {
    expect(BOLGIA_13.dimensione).toBe(13);
    expect(BOLGIA_13.composizione).toEqual([{ n: 6, giocatori: 3 }, { n: 4, giocatori: 4 }]);
  });
});
```

- [ ] **Step 2: Run test to verify it fails**

Run: `pnpm -C "/Users/paolo/Server/Siti Web/Lavoro/TtA" test`
Expected: FAIL — modulo `./templates` inesistente.

- [ ] **Step 3: Write the implementation**

Crea `src/lib/server/config/templates.ts`:

```ts
import type { GironiConfig } from './phase-config';

export const BOLGIA_13: GironiConfig = {
  kind: 'GIRONI_ROUND_ROBIN',
  dimensione: 13,
  incontriPerCoppia: 2,
  composizione: [{ n: 6, giocatori: 3 }, { n: 4, giocatori: 4 }],
  tabellaPunti: { '2': [4, 0], '3': [10, 4, 0], '4': [11, 6, 2, 0] },
  coda: { multipliOttimali: [52, 65, 77] }
};

export const BOLGIA_7: GironiConfig = {
  kind: 'GIRONI_ROUND_ROBIN',
  dimensione: 7,
  incontriPerCoppia: 1,
  composizione: [{ n: 6, giocatori: 3 }],
  tabellaPunti: { '3': [10, 4, 0] }
};

export const BOLGIA_4: GironiConfig = {
  kind: 'GIRONI_ROUND_ROBIN',
  dimensione: 4,
  incontriPerCoppia: 1,
  composizione: [{ n: 3, giocatori: 2 }, { n: 3, giocatori: 3 }, { n: 2, giocatori: 4 }],
  tabellaPunti: { '2': [4, 0], '3': [10, 4, 0], '4': [11, 6, 2, 0] }
};

export const FORMAT_TEMPLATES = { BOLGIA_13, BOLGIA_7, BOLGIA_4 } as const;
```

> Nota: `incontriPerCoppia` di BOLGIA_7/4 è un default strutturalmente valido; la satisfiability del round-robin (ogni coppia N volte) sarà verificata e, se serve, corretta in **F3.1**.

- [ ] **Step 4: Run test to verify it passes**

Run: `pnpm -C "/Users/paolo/Server/Siti Web/Lavoro/TtA" test`
Expected: PASS.

- [ ] **Step 5: Commit**

```bash
git add src/lib/server/config/templates.ts src/lib/server/config/templates.test.ts
git commit -m "feat(fase3.0): FormatTemplate Bolgia 13/7/4 (costanti validate)"
```

---

## Task 3: Refactor schema in cartella + `user.ruolo` (colonna)

**Files:**
- Create: `src/lib/server/db/schema/{enums,auth,player,index}.ts`
- Delete: `src/lib/server/db/schema.ts`, `src/lib/server/db/auth-schema.ts`
- Modify: `drizzle.config.ts`

- [ ] **Step 1: Crea `src/lib/server/db/schema/enums.ts`**

```ts
import { pgEnum } from 'drizzle-orm/pg-core';

export const tipoPeriodo = pgEnum('tipo_periodo', ['estiva', 'autunnale', 'invernale']);
export const eventStato = pgEnum('event_stato', ['bozza', 'iscrizioni', 'in_corso', 'concluso']);
export const phaseKind = pgEnum('phase_kind', ['GIRONI_ROUND_ROBIN', 'BRACKET_ELIMINAZIONE', 'A_SQUADRE', 'PARTITA_SINGOLA']);
export const phaseStato = pgEnum('phase_stato', ['pending', 'seeding', 'running', 'closed']);
export const matchStato = pgEnum('match_stato', ['programmata', 'in_corso', 'conclusa', 'resign', 'timeout']);
export const sanctionTipo = pgEnum('sanction_tipo', ['timeout', 'ammonizione', 'squalifica', 'penalizzazione', 'ban']);
export const registrationStato = pgEnum('registration_stato', ['richiesta', 'confermata', 'rifiutata', 'rinuncia', 'in_coda']);
export const notificationCanale = pgEnum('notification_canale', ['email', 'push']);
export const contentTipo = pgEnum('content_tipo', ['notizia', 'pagina']);
```

- [ ] **Step 2: Crea `src/lib/server/db/schema/auth.ts`** (sposta l'ex `auth-schema.ts`, aggiunge `ruolo`)

```ts
import { relations } from 'drizzle-orm';
import { pgTable, text, timestamp, boolean, index } from 'drizzle-orm/pg-core';

export const user = pgTable('user', {
  id: text('id').primaryKey(),
  name: text('name').notNull(),
  email: text('email').notNull().unique(),
  emailVerified: boolean('email_verified').default(false).notNull(),
  image: text('image'),
  ruolo: text('ruolo').notNull().default('player'),
  createdAt: timestamp('created_at').defaultNow().notNull(),
  updatedAt: timestamp('updated_at')
    .defaultNow()
    .$onUpdate(() => /* @__PURE__ */ new Date())
    .notNull()
});

export const session = pgTable(
  'session',
  {
    id: text('id').primaryKey(),
    expiresAt: timestamp('expires_at').notNull(),
    token: text('token').notNull().unique(),
    createdAt: timestamp('created_at').defaultNow().notNull(),
    updatedAt: timestamp('updated_at')
      .$onUpdate(() => /* @__PURE__ */ new Date())
      .notNull(),
    ipAddress: text('ip_address'),
    userAgent: text('user_agent'),
    userId: text('user_id')
      .notNull()
      .references(() => user.id, { onDelete: 'cascade' })
  },
  (table) => [index('session_userId_idx').on(table.userId)]
);

export const account = pgTable(
  'account',
  {
    id: text('id').primaryKey(),
    accountId: text('account_id').notNull(),
    providerId: text('provider_id').notNull(),
    userId: text('user_id')
      .notNull()
      .references(() => user.id, { onDelete: 'cascade' }),
    accessToken: text('access_token'),
    refreshToken: text('refresh_token'),
    idToken: text('id_token'),
    accessTokenExpiresAt: timestamp('access_token_expires_at'),
    refreshTokenExpiresAt: timestamp('refresh_token_expires_at'),
    scope: text('scope'),
    password: text('password'),
    createdAt: timestamp('created_at').defaultNow().notNull(),
    updatedAt: timestamp('updated_at')
      .$onUpdate(() => /* @__PURE__ */ new Date())
      .notNull()
  },
  (table) => [index('account_userId_idx').on(table.userId)]
);

export const verification = pgTable(
  'verification',
  {
    id: text('id').primaryKey(),
    identifier: text('identifier').notNull(),
    value: text('value').notNull(),
    expiresAt: timestamp('expires_at').notNull(),
    createdAt: timestamp('created_at').defaultNow().notNull(),
    updatedAt: timestamp('updated_at')
      .defaultNow()
      .$onUpdate(() => /* @__PURE__ */ new Date())
      .notNull()
  },
  (table) => [index('verification_identifier_idx').on(table.identifier)]
);

export const userRelations = relations(user, ({ many }) => ({
  sessions: many(session),
  accounts: many(account)
}));
export const sessionRelations = relations(session, ({ one }) => ({
  user: one(user, { fields: [session.userId], references: [user.id] })
}));
export const accountRelations = relations(account, ({ one }) => ({
  user: one(user, { fields: [account.userId], references: [user.id] })
}));
```

- [ ] **Step 3: Crea `src/lib/server/db/schema/player.ts`**

```ts
import { pgTable, serial, integer, text, boolean, timestamp } from 'drizzle-orm/pg-core';
import { user } from './auth';

export const player = pgTable('player', {
  id: serial('id').primaryKey(),
  userId: text('user_id').references(() => user.id, { onDelete: 'set null' }),
  nickTdg: text('nick_tdg').unique(),
  nomeAppTta: text('nome_app_tta').notNull().unique(),
  attivo: boolean('attivo').notNull().default(true),
  note: text('note'),
  createdAt: timestamp('created_at').notNull().defaultNow()
});

export const playerAlias = pgTable('player_alias', {
  id: serial('id').primaryKey(),
  playerId: integer('player_id')
    .notNull()
    .references(() => player.id, { onDelete: 'cascade' }),
  aliasNick: text('alias_nick'),
  aliasNomeApp: text('alias_nome_app'),
  fonte: text('fonte'),
  createdAt: timestamp('created_at').notNull().defaultNow()
});
```

- [ ] **Step 4: Crea `src/lib/server/db/schema/index.ts`**

```ts
export * from './enums';
export * from './auth';
export * from './player';
```

- [ ] **Step 5: Elimina i vecchi file e aggiorna `drizzle.config.ts`**

```bash
git rm src/lib/server/db/schema.ts src/lib/server/db/auth-schema.ts
```

In `drizzle.config.ts` cambia la riga `schema`:

```ts
  schema: './src/lib/server/db/schema',
```

- [ ] **Step 6: Verifica typecheck + test esistenti**

Run: `pnpm -C "/Users/paolo/Server/Siti Web/Lavoro/TtA" check && pnpm -C "/Users/paolo/Server/Siti Web/Lavoro/TtA" test`
Expected: `check` 0/0 (gli import esistenti `from './db/schema'` risolvono alla cartella); `test` verde offline. `db/index.ts` (`import * as schema from './schema'`) e `players.ts` (`from './db/schema'`) continuano a funzionare.

- [ ] **Step 7: Commit**

```bash
git add -A src/lib/server/db drizzle.config.ts
git commit -m "refactor(fase3.0): schema in cartella db/schema + colonna user.ruolo"
```

---

## Task 4: Tabelle eventi (`season`, `event`, `phase`, `girone`)

**Files:**
- Create: `src/lib/server/db/schema/events.ts`
- Modify: `src/lib/server/db/schema/index.ts`

- [ ] **Step 1: Crea `src/lib/server/db/schema/events.ts`**

> `group` è parola riservata SQL → la tabella si chiama **`girone`** (termine di dominio).

```ts
import { pgTable, serial, integer, text, boolean, timestamp, jsonb, index } from 'drizzle-orm/pg-core';
import { tipoPeriodo, eventStato, phaseKind, phaseStato } from './enums';
import { user } from './auth';
import type { PhaseConfig, FormatConfig } from '$lib/server/config/phase-config';

export const season = pgTable('season', {
  id: serial('id').primaryKey(),
  identificativo: text('identificativo').notNull().unique(),
  tipoPeriodo: tipoPeriodo('tipo_periodo').notNull(),
  anno: integer('anno').notNull(),
  isCurrent: boolean('is_current').notNull().default(false),
  startedAt: timestamp('started_at'),
  endedAt: timestamp('ended_at'),
  createdAt: timestamp('created_at').notNull().defaultNow()
});

export const event = pgTable('event', {
  id: serial('id').primaryKey(),
  slug: text('slug').notNull().unique(),
  tipo: text('tipo').notNull(),
  seasonId: integer('season_id').references(() => season.id),
  nome: text('nome').notNull(),
  stato: eventStato('stato').notNull().default('bozza'),
  regolamentoVersione: text('regolamento_versione'),
  formatConfig: jsonb('format_config').$type<FormatConfig>(),
  createdBy: text('created_by').references(() => user.id),
  createdAt: timestamp('created_at').notNull().defaultNow()
});

export const phase = pgTable(
  'phase',
  {
    id: serial('id').primaryKey(),
    eventId: integer('event_id')
      .notNull()
      .references(() => event.id, { onDelete: 'cascade' }),
    ordine: integer('ordine').notNull(),
    kind: phaseKind('kind').notNull(),
    nome: text('nome'),
    stato: phaseStato('stato').notNull().default('pending'),
    config: jsonb('config').$type<PhaseConfig>(),
    seedingSource: jsonb('seeding_source'),
    createdAt: timestamp('created_at').notNull().defaultNow()
  },
  (t) => [index('phase_event_idx').on(t.eventId)]
);

export const girone = pgTable(
  'girone',
  {
    id: serial('id').primaryKey(),
    phaseId: integer('phase_id')
      .notNull()
      .references(() => phase.id, { onDelete: 'cascade' }),
    livello: text('livello').notNull(),
    nomeTematico: text('nome_tematico'),
    dimensione: integer('dimensione').notNull(),
    ordine: integer('ordine').notNull(),
    rankBase: integer('rank_base').notNull(),
    createdAt: timestamp('created_at').notNull().defaultNow()
  },
  (t) => [index('girone_phase_idx').on(t.phaseId)]
);
```

- [ ] **Step 2: Aggiungi l'export in `src/lib/server/db/schema/index.ts`**

```ts
export * from './enums';
export * from './auth';
export * from './player';
export * from './events';
```

- [ ] **Step 3: Verifica typecheck**

Run: `pnpm -C "/Users/paolo/Server/Siti Web/Lavoro/TtA" check`
Expected: 0 errori (il `$type<PhaseConfig>` aggancia il tipo dal config).

- [ ] **Step 4: Commit**

```bash
git add src/lib/server/db/schema/events.ts src/lib/server/db/schema/index.ts
git commit -m "feat(fase3.0): tabelle season/event/phase/girone (config JSONB tipizzato)"
```

---

## Task 5: Tabelle gioco (`match`, `matchParticipant`, `standing`)

**Files:**
- Create: `src/lib/server/db/schema/matches.ts`
- Modify: `src/lib/server/db/schema/index.ts`

- [ ] **Step 1: Crea `src/lib/server/db/schema/matches.ts`**

```ts
import { pgTable, serial, integer, text, boolean, timestamp, jsonb, doublePrecision, index } from 'drizzle-orm/pg-core';
import { matchStato } from './enums';
import { phase, girone } from './events';
import { player } from './player';

export const match = pgTable(
  'match',
  {
    id: serial('id').primaryKey(),
    phaseId: integer('phase_id')
      .notNull()
      .references(() => phase.id, { onDelete: 'cascade' }),
    gironeId: integer('girone_id').references(() => girone.id, { onDelete: 'cascade' }),
    serieIndex: integer('serie_index'),
    nGiocatori: integer('n_giocatori').notNull(),
    stato: matchStato('stato').notNull().default('programmata'),
    linkAppCge: text('link_app_cge'),
    dataAvvioPrevista: timestamp('data_avvio_prevista'),
    dataAvvioEffettiva: timestamp('data_avvio_effettiva'),
    createdAt: timestamp('created_at').notNull().defaultNow()
  },
  (t) => [index('match_phase_idx').on(t.phaseId), index('match_girone_idx').on(t.gironeId)]
);

export const matchParticipant = pgTable(
  'match_participant',
  {
    id: serial('id').primaryKey(),
    matchId: integer('match_id')
      .notNull()
      .references(() => match.id, { onDelete: 'cascade' }),
    playerId: integer('player_id').references(() => player.id, { onDelete: 'set null' }),
    isBot: boolean('is_bot').notNull().default(false),
    posizione: integer('posizione'),
    puntiCultura: integer('punti_cultura'),
    puntiTorneo: doublePrecision('punti_torneo'),
    resign: boolean('resign').notNull().default(false),
    createdAt: timestamp('created_at').notNull().defaultNow()
  },
  (t) => [index('mp_match_idx').on(t.matchId)]
);

export const standing = pgTable(
  'standing',
  {
    id: serial('id').primaryKey(),
    phaseId: integer('phase_id')
      .notNull()
      .references(() => phase.id, { onDelete: 'cascade' }),
    gironeId: integer('girone_id')
      .notNull()
      .references(() => girone.id, { onDelete: 'cascade' }),
    playerId: integer('player_id')
      .notNull()
      .references(() => player.id, { onDelete: 'cascade' }),
    puntiTorneo: doublePrecision('punti_torneo'),
    puntiSpareggio: doublePrecision('punti_spareggio'),
    posizioneFinale: integer('posizione_finale'),
    esito: jsonb('esito'),
    createdAt: timestamp('created_at').notNull().defaultNow()
  },
  (t) => [index('standing_girone_idx').on(t.gironeId)]
);
```

- [ ] **Step 2: Aggiungi l'export in `index.ts`**

```ts
export * from './matches';
```
(aggiungi la riga in coda alle altre)

- [ ] **Step 3: Verifica typecheck**

Run: `pnpm -C "/Users/paolo/Server/Siti Web/Lavoro/TtA" check`
Expected: 0 errori.

- [ ] **Step 4: Commit**

```bash
git add src/lib/server/db/schema/matches.ts src/lib/server/db/schema/index.ts
git commit -m "feat(fase3.0): tabelle match/matchParticipant/standing"
```

---

## Task 6: Tabelle storico/iscrizioni (`ranking`, `sanction`, `eventRegistration`, `notification`, `content`)

**Files:**
- Create: `src/lib/server/db/schema/ranking.ts`, `src/lib/server/db/schema/misc.ts`
- Modify: `src/lib/server/db/schema/index.ts`

- [ ] **Step 1: Crea `src/lib/server/db/schema/ranking.ts`**

```ts
import { pgTable, serial, integer, text, boolean, timestamp, jsonb, index } from 'drizzle-orm/pg-core';
import { sanctionTipo } from './enums';
import { player } from './player';
import { season, event, phase } from './events';

export const ranking = pgTable(
  'ranking',
  {
    id: serial('id').primaryKey(),
    playerId: integer('player_id')
      .notNull()
      .references(() => player.id, { onDelete: 'cascade' }),
    seasonId: integer('season_id')
      .notNull()
      .references(() => season.id, { onDelete: 'cascade' }),
    serie: text('serie'),
    posizioneGlobale: integer('posizione_globale').notNull(),
    isCoda: boolean('is_coda').notNull().default(false),
    note: text('note'),
    sourcePhaseId: integer('source_phase_id').references(() => phase.id),
    createdAt: timestamp('created_at').notNull().defaultNow()
  },
  (t) => [index('ranking_player_idx').on(t.playerId), index('ranking_season_idx').on(t.seasonId)]
);

export const sanction = pgTable('sanction', {
  id: serial('id').primaryKey(),
  playerId: integer('player_id')
    .notNull()
    .references(() => player.id, { onDelete: 'cascade' }),
  eventId: integer('event_id').references(() => event.id, { onDelete: 'set null' }),
  tipo: sanctionTipo('tipo').notNull(),
  valore: jsonb('valore'),
  giustificato: boolean('giustificato').notNull().default(false),
  note: text('note'),
  createdAt: timestamp('created_at').notNull().defaultNow()
});
```

- [ ] **Step 2: Crea `src/lib/server/db/schema/misc.ts`**

```ts
import { pgTable, serial, integer, text, timestamp, jsonb, unique, index } from 'drizzle-orm/pg-core';
import { registrationStato, notificationCanale, contentTipo } from './enums';
import { event } from './events';
import { player } from './player';
import { user } from './auth';

export const eventRegistration = pgTable(
  'event_registration',
  {
    id: serial('id').primaryKey(),
    eventId: integer('event_id')
      .notNull()
      .references(() => event.id, { onDelete: 'cascade' }),
    playerId: integer('player_id')
      .notNull()
      .references(() => player.id, { onDelete: 'cascade' }),
    stato: registrationStato('stato').notNull().default('richiesta'),
    createdAt: timestamp('created_at').notNull().defaultNow()
  },
  (t) => [unique('event_registration_unique').on(t.eventId, t.playerId), index('er_event_idx').on(t.eventId)]
);

export const notification = pgTable('notification', {
  id: serial('id').primaryKey(),
  userId: text('user_id')
    .notNull()
    .references(() => user.id, { onDelete: 'cascade' }),
  tipo: text('tipo').notNull(),
  payload: jsonb('payload'),
  canale: notificationCanale('canale').notNull().default('email'),
  inviataAt: timestamp('inviata_at'),
  lettaAt: timestamp('letta_at'),
  createdAt: timestamp('created_at').notNull().defaultNow()
});

export const content = pgTable('content', {
  id: serial('id').primaryKey(),
  titolo: text('titolo').notNull(),
  corpo: text('corpo'),
  tipo: contentTipo('tipo').notNull().default('notizia'),
  pubblicataAt: timestamp('pubblicata_at'),
  autoreId: text('autore_id').references(() => user.id, { onDelete: 'set null' }),
  createdAt: timestamp('created_at').notNull().defaultNow()
});
```

- [ ] **Step 3: Aggiorna `index.ts` (export finale completo)**

```ts
export * from './enums';
export * from './auth';
export * from './player';
export * from './events';
export * from './matches';
export * from './ranking';
export * from './misc';
```

- [ ] **Step 4: Verifica typecheck + test**

Run: `pnpm -C "/Users/paolo/Server/Siti Web/Lavoro/TtA" check && pnpm -C "/Users/paolo/Server/Siti Web/Lavoro/TtA" test`
Expected: `check` 0/0 · `test` verde offline.

- [ ] **Step 5: Commit**

```bash
git add src/lib/server/db/schema/ranking.ts src/lib/server/db/schema/misc.ts src/lib/server/db/schema/index.ts
git commit -m "feat(fase3.0): tabelle ranking/sanction/eventRegistration/notification/content"
```

---

## Task 7: Switch client Drizzle a `neon-serverless`

**Files:**
- Modify: `src/lib/server/db/index.ts`

- [ ] **Step 1: Riscrivi `src/lib/server/db/index.ts`**

```ts
import { drizzle } from 'drizzle-orm/neon-serverless';
import { Pool } from '@neondatabase/serverless';
import { env } from '$env/dynamic/private';
import * as schema from './schema';

// Pool su WebSocket → supporta transazioni (necessarie dalla logica dominio F3.2).
// Node 24 espone WebSocket globale, quindi nessun polyfill `ws` necessario.
const pool = new Pool({ connectionString: env.DATABASE_URL });
export const db = drizzle({ client: pool, schema });
```

- [ ] **Step 2: Verifica typecheck + build**

Run: `pnpm -C "/Users/paolo/Server/Siti Web/Lavoro/TtA" check && pnpm -C "/Users/paolo/Server/Siti Web/Lavoro/TtA" build`
Expected: 0 errori; build verde.

- [ ] **Step 3: ⚠️ DB — verifica connettività** *(controller)*

Run: `pnpm -C "/Users/paolo/Server/Siti Web/Lavoro/TtA" test:integration`
Expected: il test `db.integration.test.ts` (select su `player`) passa via `neon-serverless`. *(Le tabelle dominio non esistono ancora nel DB: i loro test arrivano dopo la migrazione, Task 10/11.)*
Se la connessione WS fallisse: `pnpm add ws @types/ws` e, in `db/index.ts`, `import ws from 'ws'; neonConfig.webSocketConstructor = ws;` (import `neonConfig` da `@neondatabase/serverless`).

- [ ] **Step 4: Commit**

```bash
git add src/lib/server/db/index.ts
git commit -m "feat(fase3.0): client Drizzle su neon-serverless (Pool, transazioni)"
```

---

## Task 8: Better Auth — campo `ruolo` (`additionalFields`)

**Files:**
- Modify: `src/lib/server/auth.ts`

- [ ] **Step 1: Aggiungi `additionalFields` dentro l'oggetto `user` di `betterAuth`**

In `src/lib/server/auth.ts`, nel blocco `user: { changeEmail: {...} }`, aggiungi la proprietà `additionalFields` **accanto** a `changeEmail`:

```ts
  user: {
    additionalFields: {
      ruolo: {
        type: 'string',
        required: false,
        defaultValue: 'player',
        input: false // SICUREZZA: l'utente non può impostarsi il ruolo al signup
      }
    },
    changeEmail: {
      enabled: true,
      sendChangeEmailConfirmation: async ({ user, newEmail, url }) => {
        await sendEmail(
          user.email,
          'Conferma il cambio email',
          `Hai richiesto di cambiare l'email del tuo account in ${newEmail}.\n` +
            `Conferma cliccando questo link dal tuo indirizzo ATTUALE: ${url}\n` +
            `Se non hai richiesto tu il cambio, ignora questa email.`
        );
      }
    }
  },
```

- [ ] **Step 2: Verifica typecheck + build**

Run: `pnpm -C "/Users/paolo/Server/Siti Web/Lavoro/TtA" check && pnpm -C "/Users/paolo/Server/Siti Web/Lavoro/TtA" build`
Expected: 0 errori; build verde. (`auth.$Infer.Session.user` ora include `ruolo`.)

- [ ] **Step 3: Commit**

```bash
git add src/lib/server/auth.ts
git commit -m "feat(fase3.0): campo ruolo su user via Better Auth additionalFields (input:false)"
```

---

## Task 9: Autorizzazione (`authz.ts`) + sessione in `event.locals`

**Files:**
- Create: `src/lib/server/authz.ts`, `src/lib/server/authz.test.ts`
- Modify: `src/app.d.ts`, `src/hooks.server.ts`, `src/routes/dashboard/+page.server.ts`, `src/routes/profilo/+page.server.ts`

- [ ] **Step 1: Write the failing test**

Crea `src/lib/server/authz.test.ts`:

```ts
import { describe, it, expect } from 'vitest';
import { hasRole } from './authz';

describe('hasRole', () => {
  it('false se utente assente', () => {
    expect(hasRole(null, 'player')).toBe(false);
  });
  it('gerarchia player < admin < superadmin < developer', () => {
    expect(hasRole({ ruolo: 'player' }, 'player')).toBe(true);
    expect(hasRole({ ruolo: 'player' }, 'admin')).toBe(false);
    expect(hasRole({ ruolo: 'admin' }, 'admin')).toBe(true);
    expect(hasRole({ ruolo: 'developer' }, 'superadmin')).toBe(true);
  });
  it('ruolo sconosciuto trattato come player', () => {
    expect(hasRole({ ruolo: 'xyz' }, 'admin')).toBe(false);
    expect(hasRole({ ruolo: 'xyz' }, 'player')).toBe(true);
  });
});
```

- [ ] **Step 2: Run test to verify it fails**

Run: `pnpm -C "/Users/paolo/Server/Siti Web/Lavoro/TtA" test`
Expected: FAIL — modulo `./authz` inesistente.

- [ ] **Step 3: Crea `src/lib/server/authz.ts`**

```ts
import { error, redirect } from '@sveltejs/kit';

export const RUOLI = ['player', 'admin', 'superadmin', 'developer'] as const;
export type Ruolo = (typeof RUOLI)[number];

function rank(ruolo: string | null | undefined): number {
  const i = RUOLI.indexOf((ruolo ?? 'player') as Ruolo);
  return i < 0 ? 0 : i; // sconosciuto => livello player
}

export function hasRole(user: { ruolo?: string | null } | null, min: Ruolo): boolean {
  if (!user) return false;
  return rank(user.ruolo) >= rank(min);
}

/** Throwa redirect a /login se non loggato, error 403 se ruolo insufficiente. Ritorna l'utente. */
export function requireRole(locals: App.Locals, min: Ruolo) {
  if (!locals.user) throw redirect(302, '/login');
  if (!hasRole(locals.user, min)) throw error(403, 'Permesso negato');
  return locals.user;
}

export const requireAdmin = (locals: App.Locals) => requireRole(locals, 'admin');
```

- [ ] **Step 4: Run test to verify it passes**

Run: `pnpm -C "/Users/paolo/Server/Siti Web/Lavoro/TtA" test`
Expected: PASS.

- [ ] **Step 5: Tipizza `src/app.d.ts`**

```ts
// See https://svelte.dev/docs/kit/types#app.d.ts
import type { auth } from '$lib/server/auth';

declare global {
  namespace App {
    interface Locals {
      user: (typeof auth.$Infer.Session)['user'] | null;
      session: (typeof auth.$Infer.Session)['session'] | null;
    }
  }
}

export {};
```

- [ ] **Step 6: Popola i `locals` in `src/hooks.server.ts`**

```ts
import { auth } from '$lib/server/auth';
import { svelteKitHandler } from 'better-auth/svelte-kit';
import { building } from '$app/environment';

export async function handle({ event, resolve }) {
  if (!building) {
    const session = await auth.api.getSession({ headers: event.request.headers });
    event.locals.user = session?.user ?? null;
    event.locals.session = session?.session ?? null;
  }
  return svelteKitHandler({ event, resolve, auth, building });
}
```

- [ ] **Step 7: Refactor `dashboard/+page.server.ts` per usare `locals`**

```ts
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';

export const load: PageServerLoad = async ({ locals }) => {
  if (!locals.user) throw redirect(302, '/login');
  return { user: locals.user };
};
```

- [ ] **Step 8: Refactor `profilo/+page.server.ts` per usare `locals`**

Sostituisci le due chiamate `auth.api.getSession(...)` con `locals.user` (load **e** action), mantenendo invariata la logica:

```ts
import { getPlayerByUserId, createPlayerForUser, updatePlayerIdentity } from '$lib/server/players';
import { fail, redirect } from '@sveltejs/kit';
import type { Actions, PageServerLoad } from './$types';

export const load: PageServerLoad = async ({ locals }) => {
  if (!locals.user) throw redirect(302, '/login');

  let player = await getPlayerByUserId(locals.user.id);
  if (!player) {
    try {
      await createPlayerForUser(locals.user.id, locals.user.name);
      player = await getPlayerByUserId(locals.user.id);
    } catch (err) {
      console.error('[profilo] self-heal player fallito per', locals.user.id, err);
    }
  }

  return {
    email: locals.user.email,
    emailVerified: locals.user.emailVerified,
    nomeAppTta: player?.nomeAppTta ?? locals.user.name,
    nickTdg: player?.nickTdg ?? null
  };
};

export const actions: Actions = {
  updateIdentity: async ({ request, locals }) => {
    if (!locals.user) throw redirect(302, '/login');

    const form = await request.formData();
    const nomeAppTta = String(form.get('nomeAppTta') ?? '');
    const nickTdgRaw = String(form.get('nickTdg') ?? '').trim();
    const nickTdg = nickTdgRaw === '' ? null : nickTdgRaw;

    const result = await updatePlayerIdentity(locals.user.id, { nomeAppTta, nickTdg });
    if (!result.ok) {
      return fail(400, { error: result.error, nomeAppTta, nickTdg });
    }
    return { success: true, nomeAppTta, nickTdg };
  }
};
```

- [ ] **Step 9: Verifica typecheck + build**

Run: `pnpm -C "/Users/paolo/Server/Siti Web/Lavoro/TtA" check && pnpm -C "/Users/paolo/Server/Siti Web/Lavoro/TtA" build`
Expected: 0 errori; build verde.

- [ ] **Step 10: Commit**

```bash
git add src/lib/server/authz.ts src/lib/server/authz.test.ts src/app.d.ts src/hooks.server.ts src/routes/dashboard/+page.server.ts src/routes/profilo/+page.server.ts
git commit -m "feat(fase3.0): guardia ruoli (authz) + sessione in event.locals (chiude debito guardia)"
```

---

## Task 10: Migrazioni versionate + baseline su Neon

**Files:**
- Modify: `package.json`
- Create: `drizzle/` (file generati)

- [ ] **Step 1: Aggiungi gli script in `package.json`**

Dentro `"scripts"`, accanto a `db:push`:

```json
		"db:generate": "drizzle-kit generate",
		"db:migrate": "drizzle-kit migrate"
```

- [ ] **Step 2: Genera la migrazione iniziale (offline, non tocca il DB)**

Run: `node node_modules/drizzle-kit/bin.cjs generate --name fase3_fondamenta`
Expected: crea `drizzle/0000_*.sql` (CREATE TYPE per gli enum + CREATE TABLE per tutte le tabelle, incl. `user.ruolo`) e `drizzle/meta/`. Apri il file e verifica a colpo d'occhio che contenga le ~17 tabelle (auth+player+dominio) e i 9 enum.

- [ ] **Step 3: ⚠️ DB — baseline pulita + migrate** *(controller, autorizzato; DB a 0 dati reali)*

Il DB è stato creato con `push` (nessuno storico migrazioni) e contiene già le tabelle auth/player. Per adottare le migrazioni versionate da zero, si resetta lo schema (0 dati reali) e si applica la migrazione:

```bash
cd "/Users/paolo/Server/Siti Web/Lavoro/TtA"
# 1) reset schema public (DISTRUTTIVO ma sicuro: 0 utenti/0 player verificato)
node --env-file=.env -e "import('@neondatabase/serverless').then(async ({neon})=>{const sql=neon(process.env.DATABASE_URL);await sql\`drop schema public cascade\`;await sql\`create schema public\`;console.log('schema public ricreato');})"
# 2) applica la migrazione versionata
node --env-file=.env node_modules/drizzle-kit/bin.cjs migrate
```
Expected: `drop/create schema` ok; `migrate` applica `0000_fase3_fondamenta` e crea `__drizzle_migrations`. Nessun errore.

- [ ] **Step 4: ⚠️ DB — verifica tabelle** *(controller)*

```bash
cd "/Users/paolo/Server/Siti Web/Lavoro/TtA" && node --env-file=.env -e "import('@neondatabase/serverless').then(async ({neon})=>{const sql=neon(process.env.DATABASE_URL);const r=await sql\`select table_name from information_schema.tables where table_schema='public' order by table_name\`;console.log(r.map(x=>x.table_name).join(', '));})"
```
Expected: elenco contiene `account, content, event, event_registration, girone, match, match_participant, notification, phase, player, player_alias, ranking, sanction, season, session, standing, user, verification` (+ `__drizzle_migrations`).

- [ ] **Step 5: Commit**

```bash
git add package.json drizzle/
git commit -m "chore(fase3.0): migrazioni versionate (generate+migrate) + baseline schema dominio"
```

---

## Task 11: Test d'integrazione dominio + gate finali

**Files:**
- Create: `src/lib/server/db/domain.integration.test.ts`

- [ ] **Step 1: Crea `src/lib/server/db/domain.integration.test.ts`**

```ts
import { describe, it, expect, afterAll } from 'vitest';
import { eq } from 'drizzle-orm';

const hasDb = !!process.env.DATABASE_URL;

describe.skipIf(!hasDb)('schema dominio (integration)', () => {
  const suffix = String(Date.now());
  let eventId: number;
  let playerId: number;

  afterAll(async () => {
    const { db } = await import('./index');
    const { event, player, season } = await import('./schema');
    if (eventId) await db.delete(event).where(eq(event.id, eventId)); // cascade su phase/girone/match
    if (playerId) await db.delete(player).where(eq(player.id, playerId));
    await db.delete(season).where(eq(season.identificativo, `TEST${suffix}`));
  });

  it('inserisce season → event → phase → girone → match → participant e rilegge', async () => {
    const { db } = await import('./index');
    const { season, event, phase, girone, match, matchParticipant, player } = await import('./schema');
    const { BOLGIA_13 } = await import('$lib/server/config/templates');

    const [s] = await db.insert(season).values({ identificativo: `TEST${suffix}`, tipoPeriodo: 'estiva', anno: 2027 }).returning();
    const [e] = await db.insert(event).values({ slug: `test-${suffix}`, tipo: 'BOLGIA', seasonId: s.id, nome: 'Test', formatConfig: BOLGIA_13 }).returning();
    eventId = e.id;
    const [ph] = await db.insert(phase).values({ eventId: e.id, ordine: 1, kind: 'GIRONI_ROUND_ROBIN', config: BOLGIA_13 }).returning();
    const [g] = await db.insert(girone).values({ phaseId: ph.id, livello: 'A', dimensione: 13, ordine: 1, rankBase: 1 }).returning();
    const [p] = await db.insert(player).values({ nomeAppTta: `TestEnginePlayer ${suffix}` }).returning();
    playerId = p.id;
    const [m] = await db.insert(match).values({ phaseId: ph.id, gironeId: g.id, nGiocatori: 3 }).returning();
    await db.insert(matchParticipant).values({ matchId: m.id, playerId: p.id, posizione: 1, puntiCultura: 200, puntiTorneo: 10 });

    // round-trip: il JSONB config torna tipizzato
    const [phRead] = await db.select().from(phase).where(eq(phase.id, ph.id));
    expect(phRead.kind).toBe('GIRONI_ROUND_ROBIN');
    expect(phRead.config?.kind === 'GIRONI_ROUND_ROBIN' && phRead.config.dimensione).toBe(13);
    const parts = await db.select().from(matchParticipant).where(eq(matchParticipant.matchId, m.id));
    expect(parts).toHaveLength(1);
    expect(parts[0].puntiTorneo).toBe(10);
  });

  it('eventRegistration rispetta unique(eventId, playerId)', async () => {
    const { db } = await import('./index');
    const { eventRegistration } = await import('./schema');
    await db.insert(eventRegistration).values({ eventId, playerId });
    await expect(db.insert(eventRegistration).values({ eventId, playerId })).rejects.toThrow();
  });
});
```

- [ ] **Step 2: ⚠️ DB — esegui i test d'integrazione** *(controller)*

Run: `pnpm -C "/Users/paolo/Server/Siti Web/Lavoro/TtA" test:integration`
Expected: verde (db connectivity + dominio round-trip + unique). Cleanup automatico in `afterAll`.

- [ ] **Step 3: ⚠️ DB — E2E auth dopo reset** *(controller)*

Con `pnpm dev` attivo, registra un nuovo utente da `/register` e verifica che l'account e il player vengano creati e che `ruolo` sia `player`:

```bash
cd "/Users/paolo/Server/Siti Web/Lavoro/TtA" && node --env-file=.env -e "import('@neondatabase/serverless').then(async ({neon})=>{const sql=neon(process.env.DATABASE_URL);const r=await sql\`select u.email, u.ruolo, p.nome_app_tta from \\\"user\\\" u left join player p on p.user_id=u.id order by u.created_at desc limit 3\`;console.table(r);})"
```
Expected: l'utente recente ha `ruolo='player'` e un player collegato (il flusso Fase 2b regge dopo lo switch a neon-serverless e il reset schema). Poi elimina l'utente di test.

- [ ] **Step 4: Gate finali offline**

Run: `pnpm -C "/Users/paolo/Server/Siti Web/Lavoro/TtA" check && pnpm -C "/Users/paolo/Server/Siti Web/Lavoro/TtA" test && pnpm -C "/Users/paolo/Server/Siti Web/Lavoro/TtA" build`
Expected: `check` 0/0 · `test` verde (unit offline) · `build` ok.

- [ ] **Step 5: Commit**

```bash
git add src/lib/server/db/domain.integration.test.ts
git commit -m "test(fase3.0): integrazione round-trip schema dominio + unique iscrizioni"
```

- [ ] **Step 6: Review finale + chiusura**

Lancia la review avversariale multi-dimensione (sicurezza, correttezza, spec-compliance, type/SvelteKit) sul diff `main...HEAD`, applica eventuali fix, poi usa **superpowers:finishing-a-development-branch** (merge su `main` + deploy prod Vercel + aggiornamento memorie).

---

## Self-review del piano

- **Copertura spec:** schema V1 (Task 3–6) ✓ · `user.ruolo` (Task 3 colonna + Task 8 Better Auth) ✓ · enum `kind` completo (Task 3 enums) ✓ · Zod discriminato + refine ≥ (Task 1) ✓ · FormatTemplate Bolgia (Task 2) ✓ · neon-serverless (Task 7) ✓ · migrazioni versionate + baseline (Task 10) ✓ · authz + locals + refactor dashboard/profilo (Task 9) ✓ · test unit+integration (Task 1/2/9/11) ✓ · YAGNI Team/bracket (assenti ovunque) ✓.
- **Niente placeholder:** ogni step ha codice/comando reale.
- **Coerenza tipi:** `PhaseConfig`/`GironiConfig`/`FormatConfig` (Task 1) usati in `events.ts` `$type` (Task 4) e nel test dominio (Task 11); tabella `girone` (non `group`) coerente tra `events.ts`/`matches.ts`/test; `hasRole`/`requireRole`/`Ruolo` coerenti (Task 9); `db`/`schema` import path stabile dopo refactor (Task 3) e usato in Task 7/11.
- **Ordine DB:** tutti i passi ⚠️ DB stanno in Task 7/10/11 (dopo che schema+auth+migrazione sono pronti); i subagent eseguono solo Task 1–9 offline + scrittura file di Task 10/11, fermandosi ai passi ⚠️ DB che esegue il controller.
- **Rischi coperti:** WS neon-serverless (Task 7 fallback `ws`); baseline distruttiva ma sicura e autorizzata (Task 10); Better Auth + neon-serverless E2E (Task 11.3).
</content>
