pediatric-ai-scribe-v3/migrations/1777800000000_generated-images.js

55 lines
4.9 KiB
JavaScript

// 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();
`);