@team-semicolon/semicolony-cli 4.18.51 → 4.18.52

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,44 @@
1
+ -- 149_hive_cron_capability_templates.sql
2
+ -- SEMO Ops Hive cron parity: reviewed, non-executable capability templates.
3
+ --
4
+ -- This table stores policy template metadata only. It does not enqueue, grant, enable,
5
+ -- execute, or retire any cron job. The migration runner wraps this file in one
6
+ -- transaction, so do not add BEGIN/COMMIT here.
7
+
8
+ CREATE TABLE IF NOT EXISTS semicolony.cron_capability_templates (
9
+ bot_id TEXT NOT NULL,
10
+ job_id TEXT NOT NULL,
11
+ runtime_source TEXT NOT NULL,
12
+ permission TEXT NOT NULL CHECK (permission IN ('read', 'make', 'operate')),
13
+ resource_scopes TEXT[] NOT NULL CHECK (cardinality(resource_scopes) > 0),
14
+ destination TEXT NOT NULL,
15
+ timeout_seconds INTEGER NOT NULL CHECK (timeout_seconds > 0 AND timeout_seconds <= 3600),
16
+ idempotency_key_template TEXT NOT NULL,
17
+ expected_outcome_digest TEXT NOT NULL CHECK (expected_outcome_digest ~ '^sha256:[a-f0-9]{64}$'),
18
+ source_hash TEXT NOT NULL CHECK (source_hash ~ '^sha256:[a-f0-9]{64}$'),
19
+ legacy_profile TEXT NOT NULL,
20
+ legacy_job_id TEXT NOT NULL,
21
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
22
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
23
+ PRIMARY KEY (bot_id, job_id, runtime_source),
24
+ CHECK (bot_id = 'semi'),
25
+ CHECK (runtime_source = 'hive-cron-v2'),
26
+ CHECK (legacy_job_id = job_id),
27
+ CHECK (idempotency_key_template = 'cron:' || job_id || ':{scheduled_for}'),
28
+ CHECK (NOT ('*' = ANY(resource_scopes))),
29
+ CHECK (array_position(resource_scopes, NULL) IS NULL),
30
+ FOREIGN KEY (bot_id, job_id)
31
+ REFERENCES semicolony.bot_cron_jobs(bot_id, job_id)
32
+ );
33
+
34
+ COMMENT ON TABLE semicolony.cron_capability_templates IS
35
+ 'Non-executable reviewed capability templates for disabled Hive cron parity imports.';
36
+
37
+ COMMENT ON COLUMN semicolony.cron_capability_templates.permission IS
38
+ 'Reviewed maximum permission for a scheduled occurrence lease: read | make | operate.';
39
+
40
+ COMMENT ON COLUMN semicolony.cron_capability_templates.resource_scopes IS
41
+ 'Explicit resource scopes used to mint a future per-occurrence lease. No wildcard scopes.';
42
+
43
+ CREATE INDEX IF NOT EXISTS idx_cron_capability_templates_legacy_job
44
+ ON semicolony.cron_capability_templates(legacy_profile, legacy_job_id);
@@ -0,0 +1,38 @@
1
+ -- 150_hive_cron_action_contract.sql
2
+ -- Adds a fail-closed typed action binding to reviewed Hive cron templates.
3
+ -- Existing templates remain unregistered because all three columns are nullable.
4
+ -- The migration runner supplies the transaction boundary; do not add one here.
5
+
6
+ ALTER TABLE semicolony.cron_capability_templates
7
+ ADD COLUMN IF NOT EXISTS action_kind TEXT,
8
+ ADD COLUMN IF NOT EXISTS action_version INTEGER,
9
+ ADD COLUMN IF NOT EXISTS action_contract_digest TEXT;
10
+
11
+ ALTER TABLE semicolony.cron_capability_templates
12
+ DROP CONSTRAINT IF EXISTS cron_capability_templates_action_contract_check;
13
+
14
+ ALTER TABLE semicolony.cron_capability_templates
15
+ ADD CONSTRAINT cron_capability_templates_action_contract_check CHECK (
16
+ (
17
+ action_kind IS NULL
18
+ AND action_version IS NULL
19
+ AND action_contract_digest IS NULL
20
+ )
21
+ OR
22
+ (
23
+ action_kind IS NOT NULL
24
+ AND action_version IS NOT NULL
25
+ AND action_contract_digest IS NOT NULL
26
+ AND action_version > 0
27
+ AND action_contract_digest ~ '^[a-f0-9]{64}$'
28
+ )
29
+ );
30
+
31
+ COMMENT ON COLUMN semicolony.cron_capability_templates.action_kind IS
32
+ 'Registered typed action kind; NULL means the capability template is not executable.';
33
+
34
+ COMMENT ON COLUMN semicolony.cron_capability_templates.action_version IS
35
+ 'Positive typed action contract version; NULL means unregistered.';
36
+
37
+ COMMENT ON COLUMN semicolony.cron_capability_templates.action_contract_digest IS
38
+ 'Raw lowercase SHA-256 digest of ActionContractCanonicalV1; excludes Legacy SEMO source_hash provenance.';
@@ -0,0 +1,150 @@
1
+ -- @schema-retarget: off
2
+ -- 151_hive_cron_occurrence_leases.sql
3
+ -- Durable ownership for Hive cron typed read-action occurrences.
4
+ -- The migration runner supplies the transaction boundary; do not add BEGIN/COMMIT.
5
+
6
+ CREATE TABLE IF NOT EXISTS semicolony.cron_read_action_occurrence_leases (
7
+ idempotency_key TEXT PRIMARY KEY
8
+ CHECK (idempotency_key ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$'),
9
+ claim_id TEXT NOT NULL UNIQUE
10
+ CHECK (claim_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'),
11
+ state TEXT NOT NULL DEFAULT 'acquired'
12
+ CHECK (state IN ('acquired', 'completed', 'released_failed')),
13
+ acquired_at TIMESTAMPTZ NOT NULL DEFAULT now(),
14
+ completed_at TIMESTAMPTZ,
15
+ CHECK (
16
+ (state = 'acquired' AND completed_at IS NULL)
17
+ OR (state IN ('completed', 'released_failed') AND completed_at IS NOT NULL)
18
+ )
19
+ );
20
+
21
+ COMMENT ON TABLE semicolony.cron_read_action_occurrence_leases IS
22
+ 'Durable occurrence ownership and terminal tombstones for Hive cron typed read actions.';
23
+
24
+ CREATE OR REPLACE FUNCTION semicolony.hive_cron_protect_terminal_lease()
25
+ RETURNS trigger
26
+ LANGUAGE plpgsql
27
+ SECURITY DEFINER
28
+ SET search_path = pg_catalog
29
+ AS $$
30
+ BEGIN
31
+ IF OLD.state IN ('completed', 'released_failed') THEN
32
+ RAISE EXCEPTION 'terminal occurrence lease is immutable'
33
+ USING ERRCODE = '23514';
34
+ END IF;
35
+ IF TG_OP = 'DELETE' THEN
36
+ RETURN OLD;
37
+ END IF;
38
+ RETURN NEW;
39
+ END;
40
+ $$;
41
+
42
+ DROP TRIGGER IF EXISTS hive_cron_terminal_lease_guard
43
+ ON semicolony.cron_read_action_occurrence_leases;
44
+ CREATE TRIGGER hive_cron_terminal_lease_guard
45
+ BEFORE UPDATE OR DELETE ON semicolony.cron_read_action_occurrence_leases
46
+ FOR EACH ROW
47
+ EXECUTE FUNCTION semicolony.hive_cron_protect_terminal_lease();
48
+
49
+ CREATE OR REPLACE FUNCTION semicolony.hive_cron_acquire_lease(
50
+ p_job_id text,
51
+ p_idempotency_key text,
52
+ p_claim_id text
53
+ )
54
+ RETURNS TABLE (outcome text)
55
+ LANGUAGE plpgsql
56
+ SECURITY DEFINER
57
+ SET search_path = pg_catalog
58
+ AS $$
59
+ DECLARE
60
+ inserted_count integer;
61
+ key_prefix text := 'cron:' || p_job_id || ':';
62
+ BEGIN
63
+ IF p_job_id IS NULL
64
+ OR p_idempotency_key IS NULL
65
+ OR p_claim_id IS NULL
66
+ OR pg_catalog.char_length(p_idempotency_key)
67
+ IS DISTINCT FROM pg_catalog.char_length(key_prefix) + 24
68
+ OR pg_catalog.left(p_idempotency_key, pg_catalog.char_length(key_prefix))
69
+ IS DISTINCT FROM key_prefix
70
+ OR pg_catalog.right(p_idempotency_key, 24)
71
+ !~ '^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$'
72
+ OR NOT EXISTS (
73
+ SELECT 1
74
+ FROM semicolony.cron_capability_templates AS template
75
+ WHERE template.bot_id = 'semi'
76
+ AND template.job_id = p_job_id
77
+ AND template.runtime_source = 'hive-cron-v2'
78
+ AND template.action_kind IS NOT NULL
79
+ AND template.action_version IS NOT NULL
80
+ AND template.action_contract_digest IS NOT NULL
81
+ ) THEN
82
+ RETURN;
83
+ END IF;
84
+
85
+ INSERT INTO semicolony.cron_read_action_occurrence_leases (
86
+ idempotency_key,
87
+ claim_id
88
+ )
89
+ VALUES (p_idempotency_key, p_claim_id)
90
+ ON CONFLICT (idempotency_key) DO NOTHING;
91
+ GET DIAGNOSTICS inserted_count = ROW_COUNT;
92
+
93
+ RETURN QUERY
94
+ SELECT CASE WHEN inserted_count = 1 THEN 'acquired'::text ELSE 'duplicate'::text END;
95
+ END;
96
+ $$;
97
+
98
+ CREATE OR REPLACE FUNCTION semicolony.hive_cron_release_failed(
99
+ p_job_id text,
100
+ p_idempotency_key text,
101
+ p_claim_id text
102
+ )
103
+ RETURNS TABLE (outcome text)
104
+ LANGUAGE plpgsql
105
+ SECURITY DEFINER
106
+ SET search_path = pg_catalog
107
+ AS $$
108
+ DECLARE
109
+ released_count integer;
110
+ key_prefix text := 'cron:' || p_job_id || ':';
111
+ BEGIN
112
+ IF p_job_id IS NULL
113
+ OR p_idempotency_key IS NULL
114
+ OR p_claim_id IS NULL
115
+ OR pg_catalog.char_length(p_idempotency_key)
116
+ IS DISTINCT FROM pg_catalog.char_length(key_prefix) + 24
117
+ OR pg_catalog.left(p_idempotency_key, pg_catalog.char_length(key_prefix))
118
+ IS DISTINCT FROM key_prefix
119
+ OR pg_catalog.right(p_idempotency_key, 24)
120
+ !~ '^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$'
121
+ OR NOT EXISTS (
122
+ SELECT 1
123
+ FROM semicolony.cron_capability_templates AS template
124
+ WHERE template.bot_id = 'semi'
125
+ AND template.job_id = p_job_id
126
+ AND template.runtime_source = 'hive-cron-v2'
127
+ AND template.action_kind IS NOT NULL
128
+ AND template.action_version IS NOT NULL
129
+ AND template.action_contract_digest IS NOT NULL
130
+ ) THEN
131
+ RETURN;
132
+ END IF;
133
+
134
+ UPDATE semicolony.cron_read_action_occurrence_leases
135
+ SET state = 'released_failed',
136
+ completed_at = pg_catalog.clock_timestamp()
137
+ WHERE idempotency_key = p_idempotency_key
138
+ AND claim_id = p_claim_id
139
+ AND state = 'acquired';
140
+ GET DIAGNOSTICS released_count = ROW_COUNT;
141
+
142
+ IF released_count = 1 THEN
143
+ RETURN QUERY SELECT 'released_failed'::text;
144
+ END IF;
145
+ END;
146
+ $$;
147
+
148
+ REVOKE ALL ON FUNCTION semicolony.hive_cron_protect_terminal_lease() FROM PUBLIC, sc_app;
149
+ REVOKE ALL ON FUNCTION semicolony.hive_cron_acquire_lease(text, text, text) FROM PUBLIC, sc_app;
150
+ REVOKE ALL ON FUNCTION semicolony.hive_cron_release_failed(text, text, text) FROM PUBLIC, sc_app;
@@ -0,0 +1,243 @@
1
+ -- @schema-retarget: off
2
+ -- 152_hive_cron_relay_outbox.sql
3
+ -- Digest-only persistent relay reservation and terminal tombstones.
4
+ -- The migration runner supplies the transaction boundary; do not add BEGIN/COMMIT.
5
+
6
+ CREATE TABLE IF NOT EXISTS semicolony.cron_read_action_relay_outbox (
7
+ idempotency_key TEXT PRIMARY KEY
8
+ CHECK (idempotency_key ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$'),
9
+ request_digest TEXT NOT NULL
10
+ CHECK (request_digest ~ '^[a-f0-9]{64}$'),
11
+ state TEXT NOT NULL DEFAULT 'pending'
12
+ CHECK (state IN ('pending', 'completed')),
13
+ outcome_status TEXT
14
+ CHECK (outcome_status IN ('delivered', 'suppressed', 'failed')),
15
+ error_code TEXT,
16
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
17
+ completed_at TIMESTAMPTZ,
18
+ CHECK (
19
+ (state = 'pending' AND outcome_status IS NULL AND error_code IS NULL AND completed_at IS NULL)
20
+ OR
21
+ (state = 'completed' AND outcome_status IN ('delivered', 'suppressed')
22
+ AND error_code IS NULL AND completed_at IS NOT NULL)
23
+ OR
24
+ (state = 'completed' AND outcome_status = 'failed'
25
+ AND error_code = 'delivery_failed' AND completed_at IS NOT NULL)
26
+ )
27
+ );
28
+
29
+ CREATE INDEX IF NOT EXISTS idx_cron_read_action_relay_outbox_pending_age
30
+ ON semicolony.cron_read_action_relay_outbox (created_at)
31
+ WHERE state = 'pending';
32
+
33
+ COMMENT ON TABLE semicolony.cron_read_action_relay_outbox IS
34
+ 'Digest-only relay reservation and terminal outcomes for Hive cron typed read actions.';
35
+
36
+ CREATE OR REPLACE FUNCTION semicolony.hive_cron_protect_terminal_outbox()
37
+ RETURNS trigger
38
+ LANGUAGE plpgsql
39
+ SECURITY DEFINER
40
+ SET search_path = pg_catalog
41
+ AS $$
42
+ BEGIN
43
+ IF OLD.state = 'completed' THEN
44
+ RAISE EXCEPTION 'terminal relay outbox row is immutable'
45
+ USING ERRCODE = '23514';
46
+ END IF;
47
+ IF TG_OP = 'DELETE' THEN
48
+ RETURN OLD;
49
+ END IF;
50
+ RETURN NEW;
51
+ END;
52
+ $$;
53
+
54
+ DROP TRIGGER IF EXISTS hive_cron_terminal_outbox_guard
55
+ ON semicolony.cron_read_action_relay_outbox;
56
+ CREATE TRIGGER hive_cron_terminal_outbox_guard
57
+ BEFORE UPDATE OR DELETE ON semicolony.cron_read_action_relay_outbox
58
+ FOR EACH ROW
59
+ EXECUTE FUNCTION semicolony.hive_cron_protect_terminal_outbox();
60
+
61
+ CREATE OR REPLACE FUNCTION semicolony.hive_cron_put_outbox(
62
+ p_job_id text,
63
+ p_idempotency_key text,
64
+ p_request_digest text
65
+ )
66
+ RETURNS TABLE (mutation_status text)
67
+ LANGUAGE plpgsql
68
+ SECURITY DEFINER
69
+ SET search_path = pg_catalog
70
+ AS $$
71
+ DECLARE
72
+ inserted_count integer;
73
+ key_prefix text := 'cron:' || p_job_id || ':';
74
+ BEGIN
75
+ IF p_job_id IS NULL
76
+ OR p_idempotency_key IS NULL
77
+ OR p_request_digest !~ '^[a-f0-9]{64}$'
78
+ OR pg_catalog.char_length(p_idempotency_key)
79
+ IS DISTINCT FROM pg_catalog.char_length(key_prefix) + 24
80
+ OR pg_catalog.left(p_idempotency_key, pg_catalog.char_length(key_prefix))
81
+ IS DISTINCT FROM key_prefix
82
+ OR pg_catalog.right(p_idempotency_key, 24)
83
+ !~ '^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$'
84
+ OR NOT EXISTS (
85
+ SELECT 1
86
+ FROM semicolony.cron_capability_templates AS template
87
+ WHERE template.bot_id = 'semi'
88
+ AND template.job_id = p_job_id
89
+ AND template.runtime_source = 'hive-cron-v2'
90
+ AND template.action_kind IS NOT NULL
91
+ AND template.action_version IS NOT NULL
92
+ AND template.action_contract_digest IS NOT NULL
93
+ ) THEN
94
+ RETURN;
95
+ END IF;
96
+
97
+ INSERT INTO semicolony.cron_read_action_relay_outbox AS outbox (
98
+ idempotency_key,
99
+ request_digest
100
+ )
101
+ VALUES (p_idempotency_key, p_request_digest)
102
+ ON CONFLICT (idempotency_key) DO NOTHING
103
+ ;
104
+ GET DIAGNOSTICS inserted_count = ROW_COUNT;
105
+
106
+ RETURN QUERY
107
+ SELECT CASE WHEN inserted_count = 1 THEN 'inserted'::text ELSE 'existing'::text END;
108
+ END;
109
+ $$;
110
+
111
+ CREATE OR REPLACE FUNCTION semicolony.hive_cron_get_outbox(
112
+ p_job_id text,
113
+ p_idempotency_key text,
114
+ p_request_digest text
115
+ )
116
+ RETURNS TABLE (
117
+ lookup_status text,
118
+ outcome_status text,
119
+ error_code text
120
+ )
121
+ LANGUAGE plpgsql
122
+ SECURITY DEFINER
123
+ SET search_path = pg_catalog
124
+ AS $$
125
+ DECLARE
126
+ stored_digest text;
127
+ stored_state text;
128
+ stored_outcome text;
129
+ stored_error text;
130
+ key_prefix text := 'cron:' || p_job_id || ':';
131
+ BEGIN
132
+ IF p_job_id IS NULL
133
+ OR p_idempotency_key IS NULL
134
+ OR p_request_digest !~ '^[a-f0-9]{64}$'
135
+ OR pg_catalog.char_length(p_idempotency_key)
136
+ IS DISTINCT FROM pg_catalog.char_length(key_prefix) + 24
137
+ OR pg_catalog.left(p_idempotency_key, pg_catalog.char_length(key_prefix))
138
+ IS DISTINCT FROM key_prefix
139
+ OR pg_catalog.right(p_idempotency_key, 24)
140
+ !~ '^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$'
141
+ OR NOT EXISTS (
142
+ SELECT 1
143
+ FROM semicolony.cron_capability_templates AS template
144
+ WHERE template.bot_id = 'semi'
145
+ AND template.job_id = p_job_id
146
+ AND template.runtime_source = 'hive-cron-v2'
147
+ AND template.action_kind IS NOT NULL
148
+ AND template.action_version IS NOT NULL
149
+ AND template.action_contract_digest IS NOT NULL
150
+ ) THEN
151
+ RETURN;
152
+ END IF;
153
+
154
+ SELECT outbox.request_digest,
155
+ outbox.state,
156
+ outbox.outcome_status,
157
+ outbox.error_code
158
+ INTO stored_digest, stored_state, stored_outcome, stored_error
159
+ FROM semicolony.cron_read_action_relay_outbox AS outbox
160
+ WHERE outbox.idempotency_key = p_idempotency_key;
161
+
162
+ IF NOT FOUND THEN
163
+ RETURN QUERY SELECT 'missing'::text, NULL::text, NULL::text;
164
+ ELSIF stored_digest <> p_request_digest THEN
165
+ RETURN QUERY SELECT 'digest_mismatch'::text, NULL::text, NULL::text;
166
+ ELSIF stored_state = 'pending' THEN
167
+ RETURN QUERY SELECT 'pending'::text, NULL::text, NULL::text;
168
+ ELSE
169
+ RETURN QUERY SELECT 'completed'::text, stored_outcome, stored_error;
170
+ END IF;
171
+ END;
172
+ $$;
173
+
174
+ CREATE OR REPLACE FUNCTION semicolony.hive_cron_complete_outbox(
175
+ p_job_id text,
176
+ p_idempotency_key text,
177
+ p_request_digest text,
178
+ p_outcome_status text,
179
+ p_error_code text
180
+ )
181
+ RETURNS TABLE (
182
+ mutation_status text,
183
+ outcome_status text,
184
+ error_code text
185
+ )
186
+ LANGUAGE plpgsql
187
+ SECURITY DEFINER
188
+ SET search_path = pg_catalog
189
+ AS $$
190
+ DECLARE
191
+ completed_count integer;
192
+ key_prefix text := 'cron:' || p_job_id || ':';
193
+ BEGIN
194
+ IF p_job_id IS NULL
195
+ OR p_idempotency_key IS NULL
196
+ OR p_request_digest !~ '^[a-f0-9]{64}$'
197
+ OR pg_catalog.char_length(p_idempotency_key)
198
+ IS DISTINCT FROM pg_catalog.char_length(key_prefix) + 24
199
+ OR pg_catalog.left(p_idempotency_key, pg_catalog.char_length(key_prefix))
200
+ IS DISTINCT FROM key_prefix
201
+ OR pg_catalog.right(p_idempotency_key, 24)
202
+ !~ '^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$'
203
+ OR NOT (
204
+ (p_outcome_status IN ('delivered', 'suppressed') AND p_error_code IS NULL)
205
+ OR (p_outcome_status = 'failed' AND p_error_code = 'delivery_failed')
206
+ )
207
+ OR NOT EXISTS (
208
+ SELECT 1
209
+ FROM semicolony.cron_capability_templates AS template
210
+ WHERE template.bot_id = 'semi'
211
+ AND template.job_id = p_job_id
212
+ AND template.runtime_source = 'hive-cron-v2'
213
+ AND template.action_kind IS NOT NULL
214
+ AND template.action_version IS NOT NULL
215
+ AND template.action_contract_digest IS NOT NULL
216
+ ) THEN
217
+ RETURN;
218
+ END IF;
219
+
220
+ UPDATE semicolony.cron_read_action_relay_outbox AS outbox
221
+ SET state = 'completed',
222
+ outcome_status = p_outcome_status,
223
+ error_code = p_error_code,
224
+ completed_at = pg_catalog.clock_timestamp()
225
+ WHERE outbox.idempotency_key = p_idempotency_key
226
+ AND outbox.request_digest = p_request_digest
227
+ AND outbox.state = 'pending'
228
+ ;
229
+ GET DIAGNOSTICS completed_count = ROW_COUNT;
230
+
231
+ IF completed_count = 1 THEN
232
+ RETURN QUERY SELECT 'completed'::text, p_outcome_status, p_error_code;
233
+ ELSE
234
+ RETURN QUERY SELECT 'not_completed'::text, NULL::text, NULL::text;
235
+ END IF;
236
+ END;
237
+ $$;
238
+
239
+ REVOKE ALL ON FUNCTION semicolony.hive_cron_protect_terminal_outbox() FROM PUBLIC, sc_app;
240
+ REVOKE ALL ON FUNCTION semicolony.hive_cron_put_outbox(text, text, text) FROM PUBLIC, sc_app;
241
+ REVOKE ALL ON FUNCTION semicolony.hive_cron_get_outbox(text, text, text) FROM PUBLIC, sc_app;
242
+ REVOKE ALL ON FUNCTION semicolony.hive_cron_complete_outbox(text, text, text, text, text)
243
+ FROM PUBLIC, sc_app;
@@ -0,0 +1,9 @@
1
+ -- Hive cron v2 retirement evidence must be unique across arbitrary commitment IDs.
2
+ -- The migration runner supplies the transaction boundary; do not add one here.
3
+
4
+ CREATE UNIQUE INDEX IF NOT EXISTS uq_hive_cron_evidence_idempotency
5
+ ON semicolony.bot_commitments ((metadata->>'idempotency_key'))
6
+ WHERE bot_id = 'semi'
7
+ AND source_type = 'cron'
8
+ AND runtime_source = 'hive-cron-v2'
9
+ AND metadata->>'cron_parity_runtime' = 'hive-cron-v2';