From 5431fcd3cd98196683ef7dbaf800e04ce827d8b4 Mon Sep 17 00:00:00 2001 From: mAi Date: Fri, 15 May 2026 01:37:34 +0200 Subject: [PATCH] =?UTF-8?q?feat(t-paliad-190):=20mig=20089=20=E2=80=94=20d?= =?UTF-8?q?eadline=5Frule=5Fbackfill=5Forphans=20staging?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 3 Slice 10 staging table for the fuzzy-match orphans mig 090 produces (design §3.I + m's Q10 ruling). Each legacy deadline that the matcher can't uniquely bind to a deadline_rule logs here with the full candidate list so a legal-review pass can hand-link the ambiguous tail without rerunning the match. Schema: - deadline_id FK to paliad.deadlines (ON DELETE CASCADE). - title + project_id + proceeding_code denormalised so the admin orphan-review UI groups + filters without re-joining. - reason text CHECK in ('no_match', 'ambiguous', 'no_project', 'manual_unbound'). Mig 090 writes the first two; the editor surface (Slice 11) may add the others. - candidate_count + candidate_rule_ids carry the full list of plausible rules so the legal-review UI can render "pick one" chips from the matcher's actual output. - resolved_at + resolved_rule_id flip when an editor binds the row via the admin UI; the matching paliad.deadlines.rule_id UPDATE happens at the same time. Both rows hold so the staging table doubles as an audit trail of the legal-review pass. Indexes: - deadline_id for the per-deadline lookup the admin UI uses. - unresolved_at DESC for the "open orphans" list (the only one the legal-review UI typically lists). RLS: admin-only read. The orphan list contains real deadline titles + project ids, so non-admins must not see it. Service-layer surfaces (Slice 11) gate further. Mig 089 ships the table; mig 090 does the fuzzy-match backfill + populates this table. Numbering reflects the dependency order (the backfill SELECTs INTO this table, so the table must exist first). --- ...89_deadline_rule_backfill_orphans.down.sql | 9 ++ .../089_deadline_rule_backfill_orphans.up.sql | 82 +++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 internal/db/migrations/089_deadline_rule_backfill_orphans.down.sql create mode 100644 internal/db/migrations/089_deadline_rule_backfill_orphans.up.sql diff --git a/internal/db/migrations/089_deadline_rule_backfill_orphans.down.sql b/internal/db/migrations/089_deadline_rule_backfill_orphans.down.sql new file mode 100644 index 0000000..21cd77d --- /dev/null +++ b/internal/db/migrations/089_deadline_rule_backfill_orphans.down.sql @@ -0,0 +1,9 @@ +-- t-paliad-190 down — reverses 089_deadline_rule_backfill_orphans.up.sql. +-- Drops the staging table; mig 090's down-migration MUST run first +-- (it depends on this table for its INSERT — running them in reverse +-- order satisfies that). + +DROP POLICY IF EXISTS deadline_rule_backfill_orphans_select ON paliad.deadline_rule_backfill_orphans; +DROP INDEX IF EXISTS paliad.deadline_rule_backfill_orphans_unresolved_idx; +DROP INDEX IF EXISTS paliad.deadline_rule_backfill_orphans_deadline_id_idx; +DROP TABLE IF EXISTS paliad.deadline_rule_backfill_orphans; diff --git a/internal/db/migrations/089_deadline_rule_backfill_orphans.up.sql b/internal/db/migrations/089_deadline_rule_backfill_orphans.up.sql new file mode 100644 index 0000000..9301d2b --- /dev/null +++ b/internal/db/migrations/089_deadline_rule_backfill_orphans.up.sql @@ -0,0 +1,82 @@ +-- t-paliad-190 / Fristen Phase 3 Slice 10 — staging table for the +-- fuzzy-match orphans produced by mig 090. Per design §3.I + m's Q10 +-- ruling: legacy paliad.deadlines rows whose title can't be uniquely +-- bound to a deadline_rule via fuzzy matching are NOT silently left +-- NULL — they're logged here so a legal-review pass can hand-link +-- the ambiguous tail. +-- +-- Mig 089 ships the table; mig 090 does the actual backfill + +-- populates this table. Numbering reflects the dependency order +-- (the backfill SELECTs into this table, so the table must exist +-- first). +-- +-- Schema notes: +-- - deadline_id is the FK to paliad.deadlines.id with ON DELETE +-- CASCADE so a hand-deletion of an orphan deadline cleans up +-- its staging row too. (Deadlines are normally archived, not +-- deleted; the cascade is defensive.) +-- - project_id stays denormalised so the admin orphan-review UI +-- can group orphans by project without re-joining deadlines. +-- - reason is a free-text discriminator: 'no_match' | 'ambiguous' +-- today; the editor in Slice 11 may add 'manual_unbound' or +-- similar in the future. +-- - resolved_at + resolved_rule_id are NULL on insert; the admin +-- orphan-review UI sets them when an editor hand-links the row, +-- so the table doubles as an audit trail of the legal-review +-- pass. The matching paliad.deadlines.rule_id is updated at the +-- same time (the UPDATE on deadlines fires its own audit row +-- once an audit trigger lives on that table; today no trigger, +-- so the staging row is the audit artefact). +-- +-- RLS: admin-only read. The orphan list contains real deadline titles +-- + project ids, so non-admins should not see it. The Slice 11 rule +-- editor surface gates this further. + +CREATE TABLE IF NOT EXISTS paliad.deadline_rule_backfill_orphans ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + deadline_id uuid NOT NULL + REFERENCES paliad.deadlines(id) ON DELETE CASCADE, + title text NOT NULL, + project_id uuid, + proceeding_code text, + reason text NOT NULL + CHECK (reason IN ('no_match', 'ambiguous', 'no_project', 'manual_unbound')), + candidate_count int NOT NULL DEFAULT 0, + candidate_rule_ids uuid[] NOT NULL DEFAULT '{}', + resolved_at timestamptz, + resolved_rule_id uuid + REFERENCES paliad.deadline_rules(id) ON DELETE SET NULL, + created_at timestamptz NOT NULL DEFAULT now() +); + +CREATE INDEX IF NOT EXISTS deadline_rule_backfill_orphans_deadline_id_idx + ON paliad.deadline_rule_backfill_orphans (deadline_id); + +CREATE INDEX IF NOT EXISTS deadline_rule_backfill_orphans_unresolved_idx + ON paliad.deadline_rule_backfill_orphans (created_at DESC) + WHERE resolved_at IS NULL; + +COMMENT ON TABLE paliad.deadline_rule_backfill_orphans IS + 'Slice 10 (mig 089/090, t-paliad-190): staging for legacy ' + 'paliad.deadlines rows that the fuzzy-match backfill could not ' + 'uniquely bind to a deadline_rule. Each row holds the deadline ' + 'context + the candidate rule IDs the matcher found (0 → ' + '''no_match''; ≥2 → ''ambiguous'') so a legal-review pass can ' + 'hand-link without rerunning the match. resolved_at + ' + 'resolved_rule_id flip when the admin orphan-review UI binds the ' + 'row.'; + +-- RLS: admin-only read. +ALTER TABLE paliad.deadline_rule_backfill_orphans ENABLE ROW LEVEL SECURITY; + +DROP POLICY IF EXISTS deadline_rule_backfill_orphans_select ON paliad.deadline_rule_backfill_orphans; + +CREATE POLICY deadline_rule_backfill_orphans_select + ON paliad.deadline_rule_backfill_orphans FOR SELECT + USING ( + EXISTS ( + SELECT 1 FROM paliad.users u + WHERE u.id = auth.uid() + AND u.global_role = 'global_admin' + ) + );