migrations/Version20260626120000.php line 1

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. namespace DoctrineMigrations;
  4. use Doctrine\DBAL\Schema\Schema;
  5. use Doctrine\Migrations\AbstractMigration;
  6. /**
  7.  * Creates the creator_classification table.
  8.  *
  9.  * This is the authoritative, machine-written record of a creator's
  10.  * classification as produced by the EXTERNAL classifier service. It is the
  11.  * system of record and carries the full provenance, confidence and
  12.  * reversibility of every classification:
  13.  *  - provenance: model_provider / model_id / prompt_version /
  14.  *    classification_version / input_kind / source_images / cost_usd /
  15.  *    classified_at, so every value can be traced back to the exact run that
  16.  *    wrote it;
  17.  *  - confidence: the per-attribute confidence map plus trust_score /
  18.  *    trust_band / trust_breakdown / signals, so downstream consumers can gate
  19.  *    decisions on certainty;
  20.  *  - reversibility: the prior_* columns snapshot the previous creator.* values
  21.  *    before write-back, so a classification can be cleanly restored/rolled back.
  22.  *
  23.  * It is deliberately SEPARATE from the legacy creator.* enum columns
  24.  * (creator.ethnicity, creator.age_group, creator.category_gender,
  25.  * creator.category_parent, creator.attractiveness_level, creator.tier). The
  26.  * classifier service writes its results BACK into those creator.* columns so
  27.  * the existing admin UI keeps working unchanged; those columns are a
  28.  * denormalised projection for the UI, whereas this table is the source of
  29.  * truth. The prior_* columns mirror the creator.* column types (BIGINT
  30.  * UNSIGNED) precisely so a clean restore of the legacy columns is possible.
  31.  *
  32.  * creator_id is the primary key and equals creator.id, but NO foreign key
  33.  * constraint is declared on purpose: the relation is enforced at the
  34.  * application layer so the external service can upsert rows independently of
  35.  * (and without locking against) the creator table. Because there is no FK,
  36.  * deleting a creator does NOT cascade-delete its classification row; orphan
  37.  * rows are cleaned up out-of-band by the external classifier service (or a
  38.  * periodic reconciliation job), which is the deliberate trade-off for keeping
  39.  * external-service upserts independent of the creator table.
  40.  */
  41. final class Version20260626120000 extends AbstractMigration
  42. {
  43.     public function getDescription(): string
  44.     {
  45.         return 'Create creator_classification table (authoritative record written by the external classifier service)';
  46.     }
  47.     public function up(Schema $schema): void
  48.     {
  49.         $this->addSql(<<<'SQL'
  50.             CREATE TABLE creator_classification (
  51.                 creator_id BIGINT UNSIGNED NOT NULL COMMENT 'Primary key; equals creator.id. No FK constraint by design so the external classifier service can upsert independently; the relation is enforced at the application layer.',
  52.                 ethnicity TINYINT UNSIGNED DEFAULT NULL,
  53.                 age_group TINYINT UNSIGNED DEFAULT NULL,
  54.                 category_gender TINYINT UNSIGNED DEFAULT NULL,
  55.                 category_parent TINYINT UNSIGNED DEFAULT NULL,
  56.                 attractiveness_level TINYINT UNSIGNED DEFAULT NULL,
  57.                 tier TINYINT UNSIGNED DEFAULT NULL,
  58.                 trust_score DECIMAL(5, 2) DEFAULT NULL,
  59.                 trust_band TINYINT UNSIGNED DEFAULT NULL,
  60.                 confidence JSON DEFAULT NULL,
  61.                 signals JSON DEFAULT NULL,
  62.                 trust_breakdown JSON DEFAULT NULL,
  63.                 input_kind VARCHAR(24) NOT NULL DEFAULT 'avatar_plus_posts',
  64.                 source_images JSON DEFAULT NULL,
  65.                 model_provider VARCHAR(32) DEFAULT NULL,
  66.                 model_id VARCHAR(64) DEFAULT NULL,
  67.                 prompt_version VARCHAR(16) DEFAULT NULL,
  68.                 classification_version INT NOT NULL DEFAULT 0,
  69.                 cost_usd DECIMAL(8, 5) DEFAULT NULL,
  70.                 had_human_label TINYINT(1) NOT NULL DEFAULT 0,
  71.                 prior_ethnicity BIGINT UNSIGNED DEFAULT NULL,
  72.                 prior_age_group BIGINT UNSIGNED DEFAULT NULL,
  73.                 prior_category_gender BIGINT UNSIGNED DEFAULT NULL,
  74.                 prior_category_parent BIGINT UNSIGNED DEFAULT NULL,
  75.                 prior_attractiveness_level BIGINT UNSIGNED DEFAULT NULL,
  76.                 prior_tier BIGINT UNSIGNED DEFAULT NULL,
  77.                 error TEXT DEFAULT NULL,
  78.                 classified_at DATETIME DEFAULT NULL,
  79.                 updated_at DATETIME DEFAULT NULL,
  80.                 INDEX idx_cc_ethnicity (ethnicity),
  81.                 INDEX idx_cc_age_group (age_group),
  82.                 INDEX idx_cc_tier (tier),
  83.                 INDEX idx_cc_version (classification_version),
  84.                 INDEX idx_cc_classified_at (classified_at),
  85.                 INDEX idx_cc_target (category_gender, ethnicity, tier),
  86.                 PRIMARY KEY(creator_id)
  87.             ) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB
  88.             SQL);
  89.     }
  90.     public function down(Schema $schema): void
  91.     {
  92.         $this->addSql('DROP TABLE creator_classification');
  93.     }
  94. }