@triagepilot/db 1.1.0

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.
Files changed (43) hide show
  1. package/LICENSE +105 -0
  2. package/dist/availability.d.ts +164 -0
  3. package/dist/availability.js +974 -0
  4. package/dist/database.d.ts +3 -0
  5. package/dist/database.js +9 -0
  6. package/dist/decisions.d.ts +107 -0
  7. package/dist/decisions.js +435 -0
  8. package/dist/deliveries.d.ts +30 -0
  9. package/dist/deliveries.js +82 -0
  10. package/dist/heartbeat.d.ts +11 -0
  11. package/dist/heartbeat.js +11 -0
  12. package/dist/index.d.ts +14 -0
  13. package/dist/index.js +13 -0
  14. package/dist/jobs.d.ts +86 -0
  15. package/dist/jobs.js +641 -0
  16. package/dist/kysely.d.ts +202 -0
  17. package/dist/kysely.js +1 -0
  18. package/dist/migrate.d.ts +1 -0
  19. package/dist/migrate.js +45 -0
  20. package/dist/operations.d.ts +77 -0
  21. package/dist/operations.js +256 -0
  22. package/dist/outbox.d.ts +61 -0
  23. package/dist/outbox.js +230 -0
  24. package/dist/provider-connections.d.ts +34 -0
  25. package/dist/provider-connections.js +302 -0
  26. package/dist/retention.d.ts +6 -0
  27. package/dist/retention.js +44 -0
  28. package/dist/routing-recovery.d.ts +55 -0
  29. package/dist/routing-recovery.js +175 -0
  30. package/dist/workspaces.d.ts +50 -0
  31. package/dist/workspaces.js +43 -0
  32. package/migrations/0001_initial.sql +79 -0
  33. package/migrations/0002_selected_reviewers.sql +10 -0
  34. package/migrations/0003_human_review_policy.sql +10 -0
  35. package/migrations/0004_semantic_routing_deduplication.sql +15 -0
  36. package/migrations/0005_reviewer_availability.sql +58 -0
  37. package/migrations/0005_workspace_scope.sql +198 -0
  38. package/migrations/0006_decision_outbox.sql +24 -0
  39. package/migrations/0007_workspace_reviewer_availability.sql +192 -0
  40. package/migrations/0008_reviewer_mutation_intents.sql +160 -0
  41. package/migrations/0009_provider_connection_revocations.sql +18 -0
  42. package/migrations/0010_provider_connection_preemptive_revocations.sql +12 -0
  43. package/package.json +42 -0
@@ -0,0 +1,24 @@
1
+ create table decision_outbox (
2
+ id uuid primary key default gen_random_uuid(),
3
+ workspace_id uuid not null,
4
+ decision_id uuid not null,
5
+ schema_version integer not null,
6
+ payload jsonb not null,
7
+ occurred_at timestamptz not null,
8
+ available_at timestamptz not null default now(),
9
+ published_at timestamptz,
10
+ attempt_count integer not null default 0,
11
+ last_error text,
12
+ constraint decision_outbox_workspace_fkey
13
+ foreign key (workspace_id) references workspaces(id) on delete cascade,
14
+ constraint decision_outbox_workspace_decision_fkey
15
+ foreign key (workspace_id, decision_id)
16
+ references routing_decisions(workspace_id, id) on delete cascade,
17
+ constraint decision_outbox_workspace_decision_schema_key
18
+ unique (workspace_id, decision_id, schema_version),
19
+ constraint decision_outbox_attempt_count_check check (attempt_count >= 0)
20
+ );
21
+
22
+ create index decision_outbox_claim_idx
23
+ on decision_outbox (available_at, occurred_at)
24
+ where published_at is null;
@@ -0,0 +1,192 @@
1
+ create table workspace_operational_settings (
2
+ workspace_id uuid primary key references workspaces(id) on delete cascade,
3
+ timezone text not null default 'UTC',
4
+ updated_at timestamptz not null default now()
5
+ );
6
+
7
+ insert into workspace_operational_settings (workspace_id, timezone, updated_at)
8
+ select workspaces.id, organization_settings.timezone, organization_settings.updated_at
9
+ from workspaces
10
+ cross join organization_settings
11
+ where organization_settings.id = true;
12
+
13
+ drop table organization_settings;
14
+
15
+ alter table reviewer_absences drop constraint reviewer_absences_no_overlap;
16
+ drop index reviewer_absences_active_lookup;
17
+
18
+ alter table reviewer_absences
19
+ add column workspace_id uuid,
20
+ add column provider text,
21
+ add column provider_connection_id uuid;
22
+
23
+ update reviewer_absences
24
+ set workspace_id = local_connection.workspace_id,
25
+ provider = local_connection.provider,
26
+ provider_connection_id = local_connection.id
27
+ from (
28
+ select connections.id, connections.workspace_id, connections.provider
29
+ from provider_connections connections
30
+ join workspaces on workspaces.id = connections.workspace_id
31
+ where workspaces.external_key = 'self-hosted'
32
+ and connections.status = 'active'
33
+ order by connections.created_at, connections.id
34
+ limit 1
35
+ ) local_connection;
36
+
37
+ alter table reviewer_absences rename column reviewer_handle to external_actor_id;
38
+ alter table reviewer_absences drop constraint reviewer_absences_normalized_handle;
39
+ alter table reviewer_absences alter column workspace_id set not null;
40
+ alter table reviewer_absences alter column provider set not null;
41
+ alter table reviewer_absences alter column provider_connection_id set not null;
42
+ alter table reviewer_absences
43
+ add constraint reviewer_absences_workspace_fkey
44
+ foreign key (workspace_id) references workspaces(id) on delete cascade;
45
+ alter table reviewer_absences
46
+ add constraint reviewer_absences_workspace_provider_connection_fkey
47
+ foreign key (workspace_id, provider, provider_connection_id)
48
+ references provider_connections(workspace_id, provider, id) on delete cascade;
49
+ alter table reviewer_absences
50
+ add constraint reviewer_absences_scoped_id_key
51
+ unique (workspace_id, provider, provider_connection_id, id);
52
+ alter table reviewer_absences
53
+ add constraint reviewer_absences_no_overlap exclude using gist (
54
+ workspace_id with =,
55
+ provider with =,
56
+ provider_connection_id with =,
57
+ external_actor_id with =,
58
+ tstzrange(start_at, end_at, '[)') with &&
59
+ ) where (status = 'scheduled');
60
+
61
+ create index reviewer_absences_active_lookup
62
+ on reviewer_absences (
63
+ workspace_id, provider, provider_connection_id, external_actor_id, start_at, end_at
64
+ )
65
+ where status = 'scheduled';
66
+
67
+ alter table reviewer_replacements
68
+ add column workspace_id uuid,
69
+ add column provider text,
70
+ add column provider_connection_id uuid,
71
+ add column state text not null default 'completed',
72
+ add column last_error text;
73
+
74
+ update reviewer_replacements replacements
75
+ set workspace_id = absences.workspace_id,
76
+ provider = absences.provider,
77
+ provider_connection_id = absences.provider_connection_id
78
+ from reviewer_absences absences
79
+ where absences.id = replacements.absence_id;
80
+
81
+ alter table reviewer_replacements rename column unavailable_reviewer to unavailable_actor_id;
82
+ alter table reviewer_replacements rename column replacement_reviewer to replacement_actor_id;
83
+ alter table reviewer_replacements alter column workspace_id set not null;
84
+ alter table reviewer_replacements alter column provider set not null;
85
+ alter table reviewer_replacements alter column provider_connection_id set not null;
86
+ alter table reviewer_replacements drop constraint reviewer_replacements_absence_id_fkey;
87
+ alter table reviewer_replacements drop constraint reviewer_replacements_decision_id_fkey;
88
+ do $$
89
+ declare
90
+ historical_source_key name;
91
+ begin
92
+ select constraint_name into historical_source_key
93
+ from information_schema.table_constraints
94
+ where table_schema = 'public'
95
+ and table_name = 'reviewer_replacements'
96
+ and constraint_type = 'UNIQUE';
97
+ execute format(
98
+ 'alter table reviewer_replacements drop constraint %I',
99
+ historical_source_key
100
+ );
101
+ end
102
+ $$;
103
+ alter table reviewer_replacements
104
+ add constraint reviewer_replacements_workspace_fkey
105
+ foreign key (workspace_id) references workspaces(id) on delete cascade;
106
+ alter table reviewer_replacements
107
+ add constraint reviewer_replacements_workspace_provider_connection_fkey
108
+ foreign key (workspace_id, provider, provider_connection_id)
109
+ references provider_connections(workspace_id, provider, id) on delete cascade;
110
+ alter table reviewer_replacements
111
+ add constraint reviewer_replacements_scoped_absence_fkey
112
+ foreign key (workspace_id, provider, provider_connection_id, absence_id)
113
+ references reviewer_absences(workspace_id, provider, provider_connection_id, id);
114
+ alter table reviewer_replacements
115
+ add constraint reviewer_replacements_workspace_decision_fkey
116
+ foreign key (workspace_id, decision_id)
117
+ references routing_decisions(workspace_id, id) on delete cascade;
118
+ alter table reviewer_replacements
119
+ add constraint reviewer_replacements_scoped_source_key
120
+ unique (
121
+ workspace_id, provider, provider_connection_id,
122
+ absence_id, absence_revision, decision_id
123
+ );
124
+ alter table reviewer_replacements
125
+ add constraint reviewer_replacements_workspace_id_key
126
+ unique (workspace_id, id);
127
+
128
+ drop index reviewer_replacements_absence_history;
129
+ create index reviewer_replacements_absence_history
130
+ on reviewer_replacements (
131
+ workspace_id, provider, provider_connection_id, absence_id, completed_at desc, id desc
132
+ );
133
+
134
+ alter table routing_decisions add column change_request_id text;
135
+
136
+ update routing_decisions decisions
137
+ set change_request_id = outbox.payload ->> 'changeRequestId'
138
+ from decision_outbox outbox
139
+ where outbox.workspace_id = decisions.workspace_id
140
+ and outbox.decision_id = decisions.id
141
+ and outbox.schema_version = 1
142
+ and jsonb_typeof(outbox.payload -> 'changeRequestId') = 'string'
143
+ and length(outbox.payload ->> 'changeRequestId') > 0;
144
+
145
+ alter table decision_outbox
146
+ add column event_id text,
147
+ add column event_type text,
148
+ add column reviewer_replacement_id uuid;
149
+
150
+ update decision_outbox
151
+ set event_id = coalesce(
152
+ payload ->> 'eventId',
153
+ 'decision:' || decision_id::text || ':v' || schema_version::text
154
+ ),
155
+ event_type = coalesce(payload ->> 'eventType', 'routing_decision'),
156
+ payload = payload || jsonb_build_object(
157
+ 'eventId', coalesce(
158
+ payload ->> 'eventId',
159
+ 'decision:' || decision_id::text || ':v' || schema_version::text
160
+ ),
161
+ 'eventType', coalesce(payload ->> 'eventType', 'routing_decision')
162
+ );
163
+
164
+ alter table decision_outbox alter column event_id set not null;
165
+ alter table decision_outbox alter column event_type set not null;
166
+ alter table decision_outbox alter column decision_id drop not null;
167
+ alter table decision_outbox drop constraint decision_outbox_workspace_decision_fkey;
168
+ alter table decision_outbox drop constraint decision_outbox_workspace_decision_schema_key;
169
+ alter table decision_outbox
170
+ add constraint decision_outbox_workspace_decision_fkey
171
+ foreign key (workspace_id, decision_id)
172
+ references routing_decisions(workspace_id, id) on delete cascade;
173
+ alter table decision_outbox
174
+ add constraint decision_outbox_workspace_reviewer_replacement_fkey
175
+ foreign key (workspace_id, reviewer_replacement_id)
176
+ references reviewer_replacements(workspace_id, id) on delete cascade;
177
+ alter table decision_outbox
178
+ add constraint decision_outbox_exactly_one_source
179
+ check (num_nonnulls(decision_id, reviewer_replacement_id) = 1);
180
+ alter table decision_outbox
181
+ add constraint decision_outbox_event_type_check
182
+ check (event_type in ('routing_decision', 'reviewer_replacement'));
183
+ alter table decision_outbox
184
+ add constraint decision_outbox_event_source_check
185
+ check (
186
+ num_nonnulls(decision_id, reviewer_replacement_id) <> 1 or
187
+ (event_type = 'routing_decision' and decision_id is not null) or
188
+ (event_type = 'reviewer_replacement' and reviewer_replacement_id is not null)
189
+ );
190
+ alter table decision_outbox
191
+ add constraint decision_outbox_workspace_event_key
192
+ unique (workspace_id, event_id);
@@ -0,0 +1,160 @@
1
+ alter table repositories
2
+ add constraint repositories_mutation_intent_source_key
3
+ unique (workspace_id, provider, provider_connection_id, id, external_repository_id);
4
+
5
+ alter table routing_decisions
6
+ add constraint routing_decisions_mutation_intent_source_key
7
+ unique (workspace_id, id, repository_id);
8
+
9
+ create table reviewer_mutation_intents (
10
+ id uuid primary key default gen_random_uuid(),
11
+ workspace_id uuid not null references workspaces(id) on delete cascade,
12
+ provider text not null,
13
+ provider_connection_id uuid not null,
14
+ absence_id uuid not null,
15
+ absence_revision integer not null check (absence_revision > 0),
16
+ decision_id uuid not null,
17
+ repository_record_id uuid not null,
18
+ repository_id text not null check (length(btrim(repository_id)) > 0),
19
+ change_request_id text not null check (length(btrim(change_request_id)) > 0),
20
+ expected_head_revision text not null check (length(btrim(expected_head_revision)) > 0),
21
+ unavailable_actor_id text not null check (length(btrim(unavailable_actor_id)) > 0),
22
+ replacement_actor_id text not null check (length(btrim(replacement_actor_id)) > 0),
23
+ created_at timestamptz not null default now(),
24
+ constraint reviewer_mutation_intents_workspace_provider_connection_fkey
25
+ foreign key (workspace_id, provider, provider_connection_id)
26
+ references provider_connections(workspace_id, provider, id) on delete cascade,
27
+ constraint reviewer_mutation_intents_scoped_absence_fkey
28
+ foreign key (workspace_id, provider, provider_connection_id, absence_id)
29
+ references reviewer_absences(workspace_id, provider, provider_connection_id, id),
30
+ constraint reviewer_mutation_intents_workspace_decision_fkey
31
+ foreign key (workspace_id, decision_id, repository_record_id)
32
+ references routing_decisions(workspace_id, id, repository_id),
33
+ constraint reviewer_mutation_intents_scoped_repository_fkey
34
+ foreign key (workspace_id, provider, provider_connection_id, repository_record_id, repository_id)
35
+ references repositories(workspace_id, provider, provider_connection_id, id, external_repository_id),
36
+ constraint reviewer_mutation_intents_source_key
37
+ unique (workspace_id, provider_connection_id, absence_id, absence_revision, decision_id),
38
+ constraint reviewer_mutation_intents_link_key
39
+ unique (
40
+ workspace_id, provider, provider_connection_id,
41
+ absence_id, absence_revision, decision_id, id
42
+ )
43
+ );
44
+
45
+ create function validate_reviewer_mutation_intent_insert() returns trigger
46
+ language plpgsql as $$
47
+ declare
48
+ current_revision integer;
49
+ begin
50
+ select revision into current_revision
51
+ from reviewer_absences
52
+ where workspace_id = new.workspace_id
53
+ and provider = new.provider
54
+ and provider_connection_id = new.provider_connection_id
55
+ and id = new.absence_id
56
+ for update;
57
+ if current_revision is null or current_revision <> new.absence_revision then
58
+ raise exception 'reviewer mutation intent absence revision is not current';
59
+ end if;
60
+ return new;
61
+ end;
62
+ $$;
63
+
64
+ create trigger reviewer_mutation_intents_validate_insert
65
+ before insert on reviewer_mutation_intents
66
+ for each row execute function validate_reviewer_mutation_intent_insert();
67
+
68
+ create function reject_reviewer_mutation_intent_update() returns trigger
69
+ language plpgsql as $$
70
+ begin
71
+ raise exception 'reviewer mutation intents are immutable';
72
+ end;
73
+ $$;
74
+
75
+ create trigger reviewer_mutation_intents_reject_update
76
+ before update on reviewer_mutation_intents
77
+ for each row execute function reject_reviewer_mutation_intent_update();
78
+
79
+ alter table reviewer_replacements add column mutation_intent_id uuid;
80
+ alter table reviewer_replacements
81
+ add constraint reviewer_replacements_mutation_intent_fkey
82
+ foreign key (
83
+ workspace_id, provider, provider_connection_id,
84
+ absence_id, absence_revision, decision_id, mutation_intent_id
85
+ ) references reviewer_mutation_intents (
86
+ workspace_id, provider, provider_connection_id,
87
+ absence_id, absence_revision, decision_id, id
88
+ );
89
+ alter table reviewer_replacements
90
+ add constraint reviewer_replacements_replaced_intent_check
91
+ check (outcome <> 'replaced' or mutation_intent_id is not null) not valid;
92
+
93
+ alter table reviewer_replacements
94
+ add constraint reviewer_replacements_state_outcome_check
95
+ check (
96
+ (state = 'finalizer_pending'
97
+ and outcome in ('replaced', 'skipped_policy_satisfied', 'no_replacement_available')
98
+ and last_error is null)
99
+ or (state = 'completed' and outcome <> 'permanent_failure' and last_error is null)
100
+ or (state = 'permanent_failure'
101
+ and last_error is not null and length(btrim(last_error)) > 0)
102
+ ) not valid;
103
+
104
+ create function validate_reviewer_replacement_insert() returns trigger
105
+ language plpgsql as $$
106
+ begin
107
+ if new.outcome = 'permanent_failure' then
108
+ if new.state <> 'permanent_failure' or new.last_error is null or length(btrim(new.last_error)) = 0 then
109
+ raise exception 'permanent reviewer failure requires permanent_failure state and error';
110
+ end if;
111
+ elsif new.outcome in ('replaced', 'skipped_policy_satisfied', 'no_replacement_available') then
112
+ if new.state <> 'finalizer_pending' or new.last_error is not null then
113
+ raise exception 'mapped reviewer finalizer outcome requires finalizer_pending state';
114
+ end if;
115
+ elsif new.state <> 'completed' or new.last_error is not null then
116
+ raise exception 'reviewer outcome without finalizer requires completed state';
117
+ end if;
118
+ return new;
119
+ end;
120
+ $$;
121
+
122
+ create trigger reviewer_replacements_validate_insert
123
+ before insert on reviewer_replacements
124
+ for each row execute function validate_reviewer_replacement_insert();
125
+
126
+ create function validate_reviewer_replacement_update() returns trigger
127
+ language plpgsql as $$
128
+ begin
129
+ if (new.id, new.workspace_id, new.provider, new.provider_connection_id,
130
+ new.absence_id, new.absence_revision, new.decision_id,
131
+ new.unavailable_actor_id, new.replacement_actor_id, new.mutation_intent_id,
132
+ new.outcome, new.reason, new.started_at, new.completed_at)
133
+ is distinct from
134
+ (old.id, old.workspace_id, old.provider, old.provider_connection_id,
135
+ old.absence_id, old.absence_revision, old.decision_id,
136
+ old.unavailable_actor_id, old.replacement_actor_id, old.mutation_intent_id,
137
+ old.outcome, old.reason, old.started_at, old.completed_at) then
138
+ raise exception 'reviewer replacement provenance is immutable';
139
+ end if;
140
+ if old.state <> 'finalizer_pending' then
141
+ raise exception 'terminal reviewer replacement is immutable';
142
+ end if;
143
+ if new.state = 'completed' then
144
+ if new.outcome = 'permanent_failure' or new.last_error is not null then
145
+ raise exception 'completed reviewer replacement state is malformed';
146
+ end if;
147
+ elsif new.state = 'permanent_failure' then
148
+ if new.last_error is null or length(btrim(new.last_error)) = 0 then
149
+ raise exception 'permanent reviewer replacement state requires an error';
150
+ end if;
151
+ else
152
+ raise exception 'reviewer replacement may only leave finalizer_pending';
153
+ end if;
154
+ return new;
155
+ end;
156
+ $$;
157
+
158
+ create trigger reviewer_replacements_validate_update
159
+ before update on reviewer_replacements
160
+ for each row execute function validate_reviewer_replacement_update();
@@ -0,0 +1,18 @@
1
+ alter table provider_connections
2
+ add constraint provider_connections_status_check
3
+ check (status in ('active', 'suspended', 'revoked')) not valid;
4
+
5
+ create table provider_connection_revocations (
6
+ workspace_id uuid not null references workspaces(id) on delete cascade,
7
+ provider text not null,
8
+ external_connection_id text not null,
9
+ revoked_connection_id uuid not null,
10
+ revoked_at timestamptz not null,
11
+ cleanup_completed_at timestamptz,
12
+ primary key (workspace_id, provider, revoked_connection_id),
13
+ unique (workspace_id, provider, external_connection_id)
14
+ );
15
+
16
+ create index provider_connection_revocations_cleanup_idx
17
+ on provider_connection_revocations (workspace_id, revoked_at, revoked_connection_id)
18
+ where cleanup_completed_at is null;
@@ -0,0 +1,12 @@
1
+ alter table provider_connection_revocations
2
+ add column physical_connection_id uuid;
3
+
4
+ update provider_connection_revocations
5
+ set physical_connection_id = revoked_connection_id;
6
+
7
+ alter table provider_connection_revocations
8
+ alter column revoked_connection_id set default gen_random_uuid();
9
+
10
+ create unique index provider_connection_revocations_physical_key
11
+ on provider_connection_revocations (workspace_id, provider, physical_connection_id)
12
+ where physical_connection_id is not null;
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@triagepilot/db",
3
+ "version": "1.1.0",
4
+ "license": "FSL-1.1-Apache-2.0",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/TriagePilot/triage-pilot.git",
8
+ "directory": "packages/db"
9
+ },
10
+ "type": "module",
11
+ "files": [
12
+ "dist",
13
+ "migrations",
14
+ "LICENSE"
15
+ ],
16
+ "main": "./dist/index.js",
17
+ "types": "./dist/index.d.ts",
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/index.d.ts",
21
+ "import": "./dist/index.js"
22
+ }
23
+ },
24
+ "dependencies": {
25
+ "@triagepilot/contracts": "1.1.0",
26
+ "kysely": "^0.28.17",
27
+ "pg": "^8.13.1"
28
+ },
29
+ "devDependencies": {
30
+ "@types/pg": "^8.11.10"
31
+ },
32
+ "publishConfig": {
33
+ "access": "public"
34
+ },
35
+ "publishedAt": "2026-09-22T09:41:47.000Z",
36
+ "futureLicenseEffectiveAt": "2028-09-22T09:41:47.000Z",
37
+ "scripts": {
38
+ "build": "rm -rf dist && tsc -p tsconfig.json",
39
+ "check": "tsc -p tsconfig.json --noEmit",
40
+ "migrate": "tsx src/migrate.ts"
41
+ }
42
+ }