@rulemetric/local 0.14.2 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,99 @@
1
+ -- A parent only for the cross-case/job/result relationships not held by one eval.
2
+ CREATE TABLE performance_studies (
3
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(), user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
4
+ eval_target_id uuid NOT NULL REFERENCES eval_targets(id) ON DELETE CASCADE,
5
+ project_path text NOT NULL, protocol jsonb NOT NULL, protocol_hash text NOT NULL,
6
+ result jsonb, finalized_at timestamptz,
7
+ created_at timestamptz NOT NULL DEFAULT now(),
8
+ CONSTRAINT performance_study_version CHECK (protocol->>'version' = '1'),
9
+ CONSTRAINT performance_study_hash CHECK (protocol_hash ~ '^[a-f0-9]{64}$')
10
+ );
11
+ CREATE UNIQUE INDEX performance_studies_protocol_unique ON performance_studies(user_id,eval_target_id,protocol_hash);
12
+ ALTER TABLE performance_studies ENABLE ROW LEVEL SECURITY;
13
+ -- Writes go through the API validation boundary; REST clients cannot replace
14
+ -- or erase protocols to bypass frozen lineage and final-result guards.
15
+ CREATE POLICY performance_studies_owner ON performance_studies FOR SELECT USING (auth.uid()=user_id);
16
+ CREATE FUNCTION preserve_study_protocol() RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path = public, pg_temp AS $$
17
+ BEGIN
18
+ IF (to_jsonb(NEW)-'result'-'finalized_at') IS DISTINCT FROM (to_jsonb(OLD)-'result'-'finalized_at') OR OLD.result IS NOT NULL THEN
19
+ RAISE EXCEPTION 'Frozen study protocols and final results are immutable; create a new revision';
20
+ END IF;
21
+ IF NEW.result IS NULL OR NEW.finalized_at IS NULL THEN RAISE EXCEPTION 'Final result and timestamp are required together'; END IF;
22
+ RETURN NEW;
23
+ END $$;
24
+ CREATE TRIGGER performance_study_immutable BEFORE UPDATE ON performance_studies FOR EACH ROW EXECUTE FUNCTION preserve_study_protocol();
25
+
26
+ -- Route guards give a friendly response; this also fences refinement/SQL writers.
27
+ CREATE FUNCTION preserve_frozen_study_case() RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path = public, pg_temp AS $$
28
+ BEGIN
29
+ IF EXISTS (SELECT 1 FROM performance_studies s, jsonb_array_elements(s.protocol->'tasks') t
30
+ WHERE s.eval_target_id=OLD.eval_target_id AND t->>'evalId'=OLD.id::text
31
+ AND EXISTS (SELECT 1 FROM auth.users u WHERE u.id=s.user_id)) THEN
32
+ IF TG_OP='DELETE' OR NEW IS DISTINCT FROM OLD THEN
33
+ RAISE EXCEPTION 'Frozen study cases cannot be edited, retired or deleted';
34
+ END IF;
35
+ END IF;
36
+ IF TG_OP='DELETE' THEN RETURN OLD; END IF;
37
+ RETURN NEW;
38
+ END $$;
39
+ CREATE TRIGGER performance_study_case_immutable BEFORE UPDATE OR DELETE ON evals FOR EACH ROW EXECUTE FUNCTION preserve_frozen_study_case();
40
+ -- One execution budget per frozen revision. Retries resume the same job.
41
+ CREATE UNIQUE INDEX eval_jobs_study_unique ON eval_jobs(user_id,(payload->'study'->>'id')) WHERE payload->'study'->>'id' IS NOT NULL;
42
+ CREATE FUNCTION preserve_study_job_snapshot() RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path = public, pg_temp AS $$
43
+ BEGIN
44
+ IF OLD.payload->'study' IS NOT NULL AND NEW.payload IS DISTINCT FROM OLD.payload THEN
45
+ RAISE EXCEPTION 'Queued study snapshot is immutable';
46
+ END IF;
47
+ RETURN NEW;
48
+ END $$;
49
+ CREATE TRIGGER study_job_snapshot_immutable BEFORE UPDATE ON eval_jobs FOR EACH ROW EXECUTE FUNCTION preserve_study_job_snapshot();
50
+
51
+ CREATE FUNCTION preserve_study_observation() RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path = public, pg_temp AS $$
52
+ BEGIN
53
+ IF OLD.outputs->'study' IS NOT NULL AND (NEW.outputs IS DISTINCT FROM OLD.outputs OR NEW.eval_id IS DISTINCT FROM OLD.eval_id OR NEW.job_id IS DISTINCT FROM OLD.job_id OR NEW.configuration IS DISTINCT FROM OLD.configuration OR NEW.run_number IS DISTINCT FROM OLD.run_number OR NEW.iteration IS DISTINCT FROM OLD.iteration OR NEW.eval_target_id IS DISTINCT FROM OLD.eval_target_id OR NEW.eval_target_version IS DISTINCT FROM OLD.eval_target_version OR NEW.status IS DISTINCT FROM OLD.status) THEN
54
+ RAISE EXCEPTION 'Study observations are immutable; retain exclusions and append a new protocol revision';
55
+ END IF;
56
+ RETURN NEW;
57
+ END $$;
58
+ CREATE TRIGGER study_observation_immutable BEFORE UPDATE ON eval_runs FOR EACH ROW EXECUTE FUNCTION preserve_study_observation();
59
+
60
+ -- Reserve each actor invocation before spawning it. A lost acknowledgement or
61
+ -- crashed worker consumes the slot: repeating an unknown call would overspend
62
+ -- and selectively replace missing outcomes. Reservations survive job retries.
63
+ CREATE TABLE performance_study_calls (
64
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
65
+ study_id uuid NOT NULL REFERENCES performance_studies(id) ON DELETE CASCADE,
66
+ job_id uuid NOT NULL REFERENCES eval_jobs(id) ON DELETE CASCADE,
67
+ task_id text NOT NULL, arm text NOT NULL CHECK (arm IN ('baseline','treatment')),
68
+ repetition integer NOT NULL CHECK (repetition > 0),
69
+ reserved_at timestamptz NOT NULL DEFAULT now(),
70
+ UNIQUE(study_id,task_id,arm,repetition)
71
+ );
72
+ ALTER TABLE performance_study_calls ENABLE ROW LEVEL SECURITY;
73
+ CREATE POLICY performance_study_calls_owner ON performance_study_calls FOR SELECT USING (
74
+ EXISTS (SELECT 1 FROM performance_studies s WHERE s.id=study_id AND s.user_id=auth.uid())
75
+ );
76
+
77
+ -- Serialize result insertion with the existing job's terminal transition.
78
+ -- A GET sealing a terminal study cannot miss a concurrently accepted run.
79
+ CREATE FUNCTION guard_study_observation_insert() RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path = public, pg_temp AS $$
80
+ DECLARE execution_status text;
81
+ BEGIN
82
+ IF NEW.outputs->'study' IS NOT NULL THEN
83
+ SELECT status INTO execution_status FROM eval_jobs WHERE id=NEW.job_id FOR UPDATE;
84
+ IF execution_status IS DISTINCT FROM 'running' THEN
85
+ RAISE EXCEPTION 'Study observations require an active execution';
86
+ END IF;
87
+ END IF;
88
+ RETURN NEW;
89
+ END $$;
90
+ CREATE TRIGGER study_observation_active BEFORE INSERT ON eval_runs FOR EACH ROW EXECUTE FUNCTION guard_study_observation_insert();
91
+
92
+ CREATE FUNCTION preserve_study_history() RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path = public, pg_temp AS $$
93
+ BEGIN
94
+ IF EXISTS (SELECT 1 FROM auth.users WHERE id=OLD.user_id) THEN
95
+ RAISE EXCEPTION 'Frozen study history cannot be deleted while the account exists';
96
+ END IF;
97
+ RETURN OLD;
98
+ END $$;
99
+ CREATE TRIGGER performance_study_history BEFORE DELETE ON performance_studies FOR EACH ROW EXECUTE FUNCTION preserve_study_history();
@@ -0,0 +1,47 @@
1
+ -- P4/P5: pre-treatment organic task eligibility and the auditable link from a
2
+ -- supported frozen study to a later application. Legacy instruction arms are
3
+ -- intentionally not backfilled: they lack the task/configuration contract.
4
+ create table public.scoped_live_confirmations (
5
+ id uuid primary key default gen_random_uuid(),
6
+ user_id uuid not null references public.profiles(id) on delete cascade,
7
+ experiment_id uuid not null references public.instruction_experiments(id) on delete cascade,
8
+ source_study_id uuid not null references public.performance_studies(id) on delete restrict,
9
+ protocol jsonb not null,
10
+ protocol_hash text not null,
11
+ salt text not null,
12
+ status text not null default 'running' check (status in ('running','stopped')),
13
+ decision text check (decision is null or decision in ('improved','harmful','inconclusive')),
14
+ decision_detail jsonb,
15
+ started_at timestamptz not null default now(),
16
+ ended_at timestamptz,
17
+ unique (experiment_id)
18
+ );
19
+ create index scoped_live_confirmations_user_status on public.scoped_live_confirmations(user_id,status);
20
+
21
+ create table public.scoped_live_confirmation_assignments (
22
+ id uuid primary key default gen_random_uuid(),
23
+ confirmation_id uuid not null references public.scoped_live_confirmations(id) on delete cascade,
24
+ user_id uuid not null references public.profiles(id) on delete cascade,
25
+ task_id text not null,
26
+ external_session_id text not null,
27
+ arm text not null check (arm in ('treated','control')),
28
+ model text not null,
29
+ configuration_hash text not null,
30
+ assigned_at timestamptz not null default now(),
31
+ delivery_hash text,
32
+ delivered_at timestamptz,
33
+ outcome text check (outcome is null or outcome in ('completed','failed','unknown')),
34
+ outcome_detail jsonb,
35
+ outcome_at timestamptz,
36
+ unique (confirmation_id,task_id),
37
+ unique (confirmation_id,external_session_id)
38
+ );
39
+ create index scoped_live_confirmation_assignments_confirmation_arm
40
+ on public.scoped_live_confirmation_assignments(confirmation_id,arm);
41
+
42
+ alter table public.recommendation_applications
43
+ add column source_study_id uuid references public.performance_studies(id) on delete restrict,
44
+ add column scoped_confirmation_id uuid references public.scoped_live_confirmations(id) on delete restrict,
45
+ add column scoped_confirmation_assignment_id uuid references public.scoped_live_confirmation_assignments(id) on delete restrict;
46
+ create index idx_recommendation_applications_scoped_confirmation
47
+ on public.recommendation_applications(scoped_confirmation_id, scoped_confirmation_assignment_id);
@@ -0,0 +1,13 @@
1
+ -- A legacy project-wide experiment is closed when a task-level P4 protocol
2
+ -- takes over. Its old arms remain historical evidence; this reason prevents
3
+ -- the ordinary renderer from continuing to inject alongside scoped assignment.
4
+ alter table public.instruction_experiments
5
+ drop constraint instruction_experiments_stopped_reason_check;
6
+
7
+ alter table public.instruction_experiments
8
+ add constraint instruction_experiments_stopped_reason_check
9
+ check (stopped_reason in (
10
+ 'graduated', 'trial_neutral', 'trial_inconclusive', 'content_changed',
11
+ 'inactive_project', 'invalid_template', 'delivery_refused',
12
+ 'scoped_live_confirmation'
13
+ ));
@@ -0,0 +1,33 @@
1
+ -- Close insert-time gaps left by 00230. A CHECK whose expression evaluates to
2
+ -- NULL accepts the row, so the version key must be required explicitly.
3
+ alter table public.performance_studies
4
+ drop constraint performance_study_version;
5
+ alter table public.performance_studies
6
+ add constraint performance_study_version
7
+ check (protocol ? 'version' and protocol->>'version' = '1');
8
+ alter table public.performance_studies
9
+ add constraint performance_study_finalization_pair
10
+ check ((result is null) = (finalized_at is null));
11
+
12
+ create or replace function public.preserve_study_protocol() returns trigger
13
+ language plpgsql security definer set search_path = public, pg_temp as $$
14
+ begin
15
+ if tg_op = 'INSERT' then
16
+ if new.result is not null or new.finalized_at is not null then
17
+ raise exception 'Studies must be frozen before a result can be finalized';
18
+ end if;
19
+ return new;
20
+ end if;
21
+ if (to_jsonb(new)-'result'-'finalized_at') is distinct from (to_jsonb(old)-'result'-'finalized_at') or old.result is not null then
22
+ raise exception 'Frozen study protocols and final results are immutable; create a new revision';
23
+ end if;
24
+ if new.result is null or new.finalized_at is null then
25
+ raise exception 'Final result and timestamp are required together';
26
+ end if;
27
+ return new;
28
+ end $$;
29
+
30
+ drop trigger performance_study_immutable on public.performance_studies;
31
+ create trigger performance_study_immutable
32
+ before insert or update on public.performance_studies
33
+ for each row execute function public.preserve_study_protocol();
@@ -0,0 +1,20 @@
1
+ -- Scoped study rows contain experiment arms and outcomes. Browser clients may
2
+ -- read their own evidence; all writes remain behind the authenticated API.
3
+ alter table public.scoped_live_confirmations enable row level security;
4
+ alter table public.scoped_live_confirmation_assignments enable row level security;
5
+
6
+ create policy scoped_live_confirmations_select_own
7
+ on public.scoped_live_confirmations for select
8
+ using (auth.uid() = user_id);
9
+
10
+ create policy scoped_live_confirmation_assignments_select_own
11
+ on public.scoped_live_confirmation_assignments for select
12
+ using (auth.uid() = user_id);
13
+
14
+ alter table public.scoped_live_confirmation_assignments
15
+ add constraint scoped_live_delivery_pair
16
+ check ((delivery_hash is null) = (delivered_at is null)),
17
+ add constraint scoped_live_outcome_pair
18
+ check ((outcome is null) = (outcome_at is null)),
19
+ add constraint scoped_live_outcome_requires_delivery
20
+ check (outcome_at is null or delivered_at is not null);
@@ -0,0 +1,4 @@
1
+ -- Retain the actual pre-treatment environment separately from the requested
2
+ -- fingerprint; legacy assignments without this evidence cannot certify effect.
3
+ alter table public.scoped_live_confirmation_assignments
4
+ add column enrollment_evidence jsonb;
@@ -0,0 +1,3 @@
1
+ -- Follow-up delivery is auditable but never another randomized observation.
2
+ alter table public.scoped_live_confirmation_assignments
3
+ add column phase text not null default 'trial' check (phase in ('trial', 'followup'));
@@ -0,0 +1,27 @@
1
+ -- API-only writers still need immutable protocol and once-only evidence guards.
2
+ create or replace function public.guard_scoped_live_confirmation() returns trigger
3
+ language plpgsql set search_path = public as $$
4
+ begin
5
+ if row(new.id,new.user_id,new.experiment_id,new.source_study_id,new.protocol,new.protocol_hash,new.salt,new.started_at)
6
+ is distinct from row(old.id,old.user_id,old.experiment_id,old.source_study_id,old.protocol,old.protocol_hash,old.salt,old.started_at)
7
+ or old.status = 'stopped' then
8
+ raise exception 'Scoped confirmation protocol and final decision are immutable';
9
+ end if;
10
+ return new;
11
+ end $$;
12
+ create trigger scoped_live_confirmation_frozen before update on public.scoped_live_confirmations
13
+ for each row execute function public.guard_scoped_live_confirmation();
14
+
15
+ create or replace function public.guard_scoped_live_assignment() returns trigger
16
+ language plpgsql set search_path = public as $$
17
+ begin
18
+ if row(new.id,new.confirmation_id,new.user_id,new.task_id,new.external_session_id,new.arm,new.phase,new.model,new.configuration_hash,new.enrollment_evidence,new.assigned_at)
19
+ is distinct from row(old.id,old.confirmation_id,old.user_id,old.task_id,old.external_session_id,old.arm,old.phase,old.model,old.configuration_hash,old.enrollment_evidence,old.assigned_at)
20
+ or (old.delivered_at is not null and row(new.delivered_at,new.delivery_hash) is distinct from row(old.delivered_at,old.delivery_hash))
21
+ or (old.outcome_at is not null and row(new.outcome_at,new.outcome,new.outcome_detail) is distinct from row(old.outcome_at,old.outcome,old.outcome_detail)) then
22
+ raise exception 'Scoped assignment identity, delivery and outcome evidence are immutable';
23
+ end if;
24
+ return new;
25
+ end $$;
26
+ create trigger scoped_live_assignment_frozen before update on public.scoped_live_confirmation_assignments
27
+ for each row execute function public.guard_scoped_live_assignment();