// Durable jobs and private asset grants. No external calls or corpus changes. exports.up = pgm => pgm.sql(` CREATE TABLE generated_image_jobs ( id UUID PRIMARY KEY, owner_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, workflow TEXT NOT NULL CHECK (workflow IN ('clinical_assistant', 'learning_hub')), idempotency_key TEXT NOT NULL, input_hash TEXT NOT NULL, prompt_cipher TEXT NOT NULL CHECK (prompt_cipher LIKE 'enc1:%'), model TEXT NOT NULL, prompt_revision INTEGER NOT NULL, budget INTEGER NOT NULL CHECK (budget BETWEEN 1000 AND 32000), prompt_units INTEGER NOT NULL, stage TEXT NOT NULL DEFAULT 'queued' CHECK (stage IN ('queued','generating','storing','done','error','interrupted')), lease_token UUID, lease_until TIMESTAMPTZ, staged_bytes BYTEA, mime TEXT, checksum TEXT, byte_length INTEGER, error_code TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE(owner_id, workflow, idempotency_key) ); CREATE INDEX generated_image_claim ON generated_image_jobs(stage, created_at); CREATE TABLE generated_image_links ( asset_id UUID NOT NULL REFERENCES generated_image_jobs(id) ON DELETE CASCADE, content_id INTEGER NOT NULL REFERENCES learning_content(id) ON DELETE CASCADE, PRIMARY KEY(asset_id, content_id) ); CREATE FUNCTION guard_generated_image_job() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN IF (NEW.owner_id, NEW.workflow, NEW.idempotency_key, NEW.input_hash, NEW.prompt_cipher, NEW.model, NEW.prompt_revision, NEW.budget, NEW.prompt_units) IS DISTINCT FROM (OLD.owner_id, OLD.workflow, OLD.idempotency_key, OLD.input_hash, OLD.prompt_cipher, OLD.model, OLD.prompt_revision, OLD.budget, OLD.prompt_units) THEN RAISE EXCEPTION 'Image job input and ownership are immutable'; END IF; RETURN NEW; END; $$; CREATE TRIGGER generated_image_job_immutable BEFORE UPDATE ON generated_image_jobs FOR EACH ROW EXECUTE FUNCTION guard_generated_image_job(); CREATE FUNCTION guard_generated_image_link() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN IF NOT EXISTS (SELECT 1 FROM generated_image_jobs WHERE id = NEW.asset_id AND workflow = 'learning_hub' AND stage = 'done') THEN RAISE EXCEPTION 'Only Learning assets may be attached'; END IF; RETURN NEW; END; $$; CREATE TRIGGER generated_image_link_guard BEFORE INSERT OR UPDATE ON generated_image_links FOR EACH ROW EXECUTE FUNCTION guard_generated_image_link(); ALTER TABLE prompt_revisions DROP CONSTRAINT prompt_revisions_prompt_key_check; ALTER TABLE prompt_revisions ADD CONSTRAINT prompt_revisions_prompt_key_check CHECK (prompt_key IN ('prompt.hpiEncounter','prompt.hpiDictation','prompt.hpiInpatient','prompt.hospitalCourseShort','prompt.hospitalCourseLong','prompt.hospitalCourseICU','prompt.hospitalCoursePsych','prompt.chartReviewOutpatient','prompt.chartReviewSubspecialty','prompt.chartReviewED','prompt.soapFull','prompt.soapSubjective','prompt.milestoneNarrative','prompt.milestoneList','prompt.milestoneSummary','prompt.peGuideNarrative','prompt.peGuideList','prompt.refine','prompt.shortenDocument','prompt.askClarification','prompt.shadessAssessment','prompt.wellVisitNote','prompt.wellVisitShort','prompt.sickVisitNote','prompt.edEncounterStaged','prompt.edConsolidate','prompt.edFinalize','prompt.dontMissTooltip','prompt.patientEducation','clinical_assistant.system_behavior','clinical_assistant.image_behavior','learning_hub.image_behavior')); `); // Down preserves append-only Learning prompt history: run only after explicit archival/removal of that history. exports.down = pgm => pgm.sql(` DO $$ BEGIN IF EXISTS(SELECT 1 FROM prompt_revisions WHERE prompt_key = 'learning_hub.image_behavior') THEN RAISE EXCEPTION 'Learning image prompt history exists; retain migration rather than discard history'; END IF; END $$; ALTER TABLE prompt_revisions DROP CONSTRAINT prompt_revisions_prompt_key_check; ALTER TABLE prompt_revisions ADD CONSTRAINT prompt_revisions_prompt_key_check CHECK (prompt_key IN ('prompt.hpiEncounter','prompt.hpiDictation','prompt.hpiInpatient','prompt.hospitalCourseShort','prompt.hospitalCourseLong','prompt.hospitalCourseICU','prompt.hospitalCoursePsych','prompt.chartReviewOutpatient','prompt.chartReviewSubspecialty','prompt.chartReviewED','prompt.soapFull','prompt.soapSubjective','prompt.milestoneNarrative','prompt.milestoneList','prompt.milestoneSummary','prompt.peGuideNarrative','prompt.peGuideList','prompt.refine','prompt.shortenDocument','prompt.askClarification','prompt.shadessAssessment','prompt.wellVisitNote','prompt.wellVisitShort','prompt.sickVisitNote','prompt.edEncounterStaged','prompt.edConsolidate','prompt.edFinalize','prompt.dontMissTooltip','prompt.patientEducation','clinical_assistant.system_behavior','clinical_assistant.image_behavior')); DROP TABLE generated_image_links; DROP TABLE generated_image_jobs; DROP FUNCTION guard_generated_image_link(); DROP FUNCTION guard_generated_image_job(); `);