All files / web/src/db/schema linear-readiness-veto.ts

100% Statements 57/57
100% Branches 2/2
100% Functions 1/1
100% Lines 57/57

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 582x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 42x 42x 42x 42x 42x 42x 2x 2x 2x 2x  
import { createId } from '@paralleldrive/cuid2'
import { integer, sqliteTable, text, uniqueIndex } from 'drizzle-orm/sqlite-core'
import { players } from './players'
 
/**
 * Linear-readiness veto table (L3).
 *
 * Linear-readiness (which skills are "aged out" onto number sentences) is DERIVED
 * fresh at plan time and never persisted — see `lib/curriculum/linear-readiness.ts`.
 * The ONLY persisted piece is this veto: a teacher saying "not yet" for a whole
 * skill category.
 *
 * Semantics: a row's PRESENCE means the category is vetoed (kept off number
 * sentences); its ABSENCE means linear-readiness is auto-conferred. The veto is
 * sticky (no expiry) — the model is "the teacher only vetoes," so it persists until
 * lifted. Because the derivation only ever READS this table, a veto can never be
 * silently re-overridden by a later derivation run.
 *
 * Grain is per-CATEGORY (a `SkillCategoryKey`, e.g. "tenComplements"), deliberately
 * coarser than per-skill: the frontier crosses a whole category at once, and this
 * keeps the teacher's decision one tap, not a per-skill chore.
 */
export const linearReadinessVeto = sqliteTable(
  'linear_readiness_veto',
  {
    /** Primary key */
    id: text('id')
      .primaryKey()
      .$defaultFn(() => createId()),
 
    /** Player this veto applies to */
    playerId: text('player_id')
      .notNull()
      .references(() => players.id, { onDelete: 'cascade' }),
 
    /** The vetoed skill category (a `SkillCategoryKey`, e.g. "tenComplements") */
    category: text('category').notNull(),
 
    /** Optional teacher note explaining the veto */
    reason: text('reason'),
 
    /** When the veto was created */
    vetoedAt: integer('vetoed_at', { mode: 'timestamp' })
      .notNull()
      .$defaultFn(() => new Date()),
  },
  (table) => ({
    /** Only one veto per player per category */
    playerCategoryUnique: uniqueIndex('linear_readiness_veto_player_category_unique').on(
      table.playerId,
      table.category
    ),
  })
)
 
export type LinearReadinessVeto = typeof linearReadinessVeto.$inferSelect
export type NewLinearReadinessVeto = typeof linearReadinessVeto.$inferInsert