@team-semicolon/semicolony-cli 4.18.126 → 4.18.128
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.
- package/dist/bundle.js +1026 -1017
- package/migrations/224_mcp_catalog_compatibility.sql +19 -0
- package/migrations/225_meeting_action_history_preservation.sql +388 -0
- package/migrations/226_action_items_work_loop.sql +634 -0
- package/migrations/227_action_items_web_identity.sql +34 -0
- package/migrations/228_action_item_events_board_grant.sql +5 -0
- package/migrations/229_worker_asset_default_policy.sql +279 -0
- package/migrations/README.md +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
-- Compatibility marker for the Gateway/Dashboard MCP catalog rollout.
|
|
2
|
+
-- The canonical schema is owned and applied by the SEMO Ops operator runner.
|
|
3
|
+
DO $block$
|
|
4
|
+
BEGIN
|
|
5
|
+
IF NOT EXISTS (
|
|
6
|
+
SELECT 1 FROM semo_ops.schema_migrations
|
|
7
|
+
WHERE version = '222_mcp_catalog_control_plane'
|
|
8
|
+
) OR NOT EXISTS (
|
|
9
|
+
SELECT 1 FROM semo_ops.schema_migrations
|
|
10
|
+
WHERE version = '223_mcp_cohort_releases'
|
|
11
|
+
) THEN
|
|
12
|
+
RAISE EXCEPTION 'SEMO Ops MCP catalog migrations 222 and 223 must be applied first';
|
|
13
|
+
END IF;
|
|
14
|
+
IF to_regprocedure('semo_ops.mcp_catalog_for_installation(uuid)') IS NULL
|
|
15
|
+
OR to_regprocedure('semo_ops.mcp_report_runtime_status(uuid,text,uuid,text,text,text,text,timestamptz)') IS NULL THEN
|
|
16
|
+
RAISE EXCEPTION 'SEMO Ops MCP Gateway function contract is missing';
|
|
17
|
+
END IF;
|
|
18
|
+
END;
|
|
19
|
+
$block$;
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
-- 225_meeting_action_history_preservation.sql
|
|
2
|
+
-- Source baseline: Semicolony 58771bc9b75a0ed473c1c838199b48e075192c13.
|
|
3
|
+
-- 2026-09-14 audit reproduced migration 204 deleting completed follow-ups and
|
|
4
|
+
-- their ON DELETE CASCADE event histories when a meeting was re-recorded.
|
|
5
|
+
-- Preserve existing tasks and append only new descriptions. Retain proposed
|
|
6
|
+
-- changes in meetings.metadata.meeting_import.preserved_action_items for review.
|
|
7
|
+
-- Signature and ACL stay unchanged; the runner owns the transaction.
|
|
8
|
+
-- Validation: scripts/verify-meeting-action-preservation.sql on disposable PG.
|
|
9
|
+
-- Rollback: restore the 15-argument function from migration 204 (no data DDL).
|
|
10
|
+
|
|
11
|
+
CREATE OR REPLACE FUNCTION semicolony.record_meeting(
|
|
12
|
+
p_title text,
|
|
13
|
+
p_meeting_date date,
|
|
14
|
+
p_meeting_type text,
|
|
15
|
+
p_adhoc_subtype text,
|
|
16
|
+
p_visibility text,
|
|
17
|
+
p_target_domain text,
|
|
18
|
+
p_attendees jsonb,
|
|
19
|
+
p_record_body text,
|
|
20
|
+
p_transcript text,
|
|
21
|
+
p_action_items jsonb,
|
|
22
|
+
p_record_source text,
|
|
23
|
+
p_worker_id text,
|
|
24
|
+
p_device_id text,
|
|
25
|
+
p_discussion_url text DEFAULT NULL,
|
|
26
|
+
p_discussion_number integer DEFAULT NULL
|
|
27
|
+
)
|
|
28
|
+
RETURNS TABLE(meeting_id uuid, kb_id bigint, action_item_ids uuid[], duplicate boolean)
|
|
29
|
+
LANGUAGE plpgsql
|
|
30
|
+
SECURITY DEFINER
|
|
31
|
+
SET search_path = pg_catalog
|
|
32
|
+
AS $function$
|
|
33
|
+
DECLARE
|
|
34
|
+
v_meeting_id uuid;
|
|
35
|
+
v_kb_id bigint;
|
|
36
|
+
v_existing uuid;
|
|
37
|
+
v_norm text;
|
|
38
|
+
v_sensitivity text;
|
|
39
|
+
v_sub_key text;
|
|
40
|
+
v_item jsonb;
|
|
41
|
+
v_item_id uuid;
|
|
42
|
+
v_preserved jsonb;
|
|
43
|
+
v_description text;
|
|
44
|
+
v_priority text;
|
|
45
|
+
v_project text;
|
|
46
|
+
v_candidate text;
|
|
47
|
+
v_assignee_kind text;
|
|
48
|
+
v_assignee_member text;
|
|
49
|
+
v_assignee_agent text;
|
|
50
|
+
v_external_contact text;
|
|
51
|
+
v_deadline date;
|
|
52
|
+
-- Y1: fallbacks are recorded as data, not only as a log line. v_fallbacks
|
|
53
|
+
-- accumulates per item and lands in that item's own metadata; v_skipped
|
|
54
|
+
-- accumulates items that produced no row at all and lands on the meeting.
|
|
55
|
+
v_fallbacks jsonb;
|
|
56
|
+
v_skipped jsonb;
|
|
57
|
+
BEGIN
|
|
58
|
+
IF p_title IS NULL OR length(btrim(p_title)) < 3 THEN
|
|
59
|
+
RAISE EXCEPTION 'title must be at least 3 characters';
|
|
60
|
+
END IF;
|
|
61
|
+
IF p_meeting_date IS NULL THEN
|
|
62
|
+
RAISE EXCEPTION 'meeting_date is required';
|
|
63
|
+
END IF;
|
|
64
|
+
IF p_visibility IS NULL OR p_visibility NOT IN ('team', 'restricted') THEN
|
|
65
|
+
RAISE EXCEPTION 'visibility must be team or restricted, got %', p_visibility;
|
|
66
|
+
END IF;
|
|
67
|
+
IF p_record_source IS NULL
|
|
68
|
+
OR p_record_source NOT IN ('slack-worker', 'operator', 'backfill-discussion') THEN
|
|
69
|
+
RAISE EXCEPTION 'invalid record_source %', p_record_source;
|
|
70
|
+
END IF;
|
|
71
|
+
IF p_record_body IS NULL OR length(btrim(p_record_body)) = 0 THEN
|
|
72
|
+
RAISE EXCEPTION 'record_body is required';
|
|
73
|
+
END IF;
|
|
74
|
+
|
|
75
|
+
v_norm := lower(regexp_replace(btrim(p_title), '\s+', ' ', 'g'));
|
|
76
|
+
v_sensitivity := CASE WHEN p_visibility = 'restricted' THEN 'restricted' ELSE 'internal' END;
|
|
77
|
+
|
|
78
|
+
-- Serialize all writers for one normalized meeting, including first insert.
|
|
79
|
+
PERFORM pg_advisory_xact_lock(hashtextextended(
|
|
80
|
+
'meeting:' || p_meeting_date::text || ':' || v_norm, 0));
|
|
81
|
+
|
|
82
|
+
SELECT m.meeting_id INTO v_existing
|
|
83
|
+
FROM semicolony.meetings m
|
|
84
|
+
WHERE m.meeting_date = p_meeting_date
|
|
85
|
+
AND lower(regexp_replace(m.title, '\s+', ' ', 'g')) = v_norm;
|
|
86
|
+
|
|
87
|
+
-- Discussion provenance from 202_meeting_record_discussion_provenance,
|
|
88
|
+
-- unchanged: COALESCE on update so a re-record that omits the link does not
|
|
89
|
+
-- erase one an earlier submission established, and both columns written on
|
|
90
|
+
-- insert.
|
|
91
|
+
IF v_existing IS NOT NULL THEN
|
|
92
|
+
UPDATE semicolony.meetings m
|
|
93
|
+
SET record_body = COALESCE(p_record_body, m.record_body),
|
|
94
|
+
mapped_transcript = COALESCE(p_transcript, m.mapped_transcript),
|
|
95
|
+
visibility = p_visibility,
|
|
96
|
+
record_source = p_record_source,
|
|
97
|
+
target_domain = COALESCE(p_target_domain, m.target_domain),
|
|
98
|
+
attendees = COALESCE(p_attendees, m.attendees),
|
|
99
|
+
discussion_url = COALESCE(p_discussion_url, m.discussion_url),
|
|
100
|
+
discussion_number = COALESCE(p_discussion_number, m.discussion_number),
|
|
101
|
+
updated_at = now()
|
|
102
|
+
WHERE m.meeting_id = v_existing;
|
|
103
|
+
v_meeting_id := v_existing;
|
|
104
|
+
ELSE
|
|
105
|
+
INSERT INTO semicolony.meetings (
|
|
106
|
+
meeting_id, title, meeting_type, adhoc_subtype, meeting_date, attendees,
|
|
107
|
+
visibility, record_body, mapped_transcript, record_source, target_domain,
|
|
108
|
+
transcription_status, speaker_map, discussion_url, discussion_number,
|
|
109
|
+
created_at, updated_at
|
|
110
|
+
) VALUES (
|
|
111
|
+
gen_random_uuid(), btrim(p_title), p_meeting_type, p_adhoc_subtype, p_meeting_date,
|
|
112
|
+
COALESCE(p_attendees, '[]'::jsonb), p_visibility, p_record_body,
|
|
113
|
+
p_transcript, p_record_source, p_target_domain,
|
|
114
|
+
'completed', '{}'::jsonb, p_discussion_url, p_discussion_number, now(), now()
|
|
115
|
+
) RETURNING meetings.meeting_id INTO v_meeting_id;
|
|
116
|
+
END IF;
|
|
117
|
+
|
|
118
|
+
v_sub_key := to_char(p_meeting_date, 'YYYY-MM-DD') || '-' || left(v_norm, 60);
|
|
119
|
+
|
|
120
|
+
INSERT INTO semicolony.knowledge_base (
|
|
121
|
+
domain, key, sub_key, content, metadata, created_by, created_at, updated_at
|
|
122
|
+
) VALUES (
|
|
123
|
+
COALESCE(p_target_domain, 'semicolon'),
|
|
124
|
+
'meeting',
|
|
125
|
+
v_sub_key,
|
|
126
|
+
p_record_body,
|
|
127
|
+
jsonb_build_object(
|
|
128
|
+
'classification', jsonb_build_object('sensitivity', v_sensitivity),
|
|
129
|
+
'meeting_id', v_meeting_id,
|
|
130
|
+
'meeting_date', to_char(p_meeting_date, 'YYYY-MM-DD'),
|
|
131
|
+
'record_source', p_record_source
|
|
132
|
+
),
|
|
133
|
+
COALESCE(p_worker_id, 'meeting-worker'),
|
|
134
|
+
now(), now()
|
|
135
|
+
)
|
|
136
|
+
ON CONFLICT (domain, key, sub_key) DO UPDATE
|
|
137
|
+
SET content = EXCLUDED.content,
|
|
138
|
+
metadata = semicolony.knowledge_base.metadata || EXCLUDED.metadata,
|
|
139
|
+
updated_at = now()
|
|
140
|
+
RETURNING semicolony.knowledge_base.kb_id INTO v_kb_id;
|
|
141
|
+
|
|
142
|
+
-- Re-extraction is additive. The action ledger owns execution state, manual
|
|
143
|
+
-- corrections and audit history; a transcript refresh cannot replace it.
|
|
144
|
+
-- A matching description is conservative identity within one meeting.
|
|
145
|
+
-- Preserve changed proposals for review instead of overwriting assignments,
|
|
146
|
+
-- deadlines or completion. Deliberate edits use the action-item API.
|
|
147
|
+
IF p_action_items IS NOT NULL AND jsonb_typeof(p_action_items) = 'array'
|
|
148
|
+
AND jsonb_array_length(p_action_items) > 0 THEN
|
|
149
|
+
v_skipped := '[]'::jsonb;
|
|
150
|
+
v_preserved := '[]'::jsonb;
|
|
151
|
+
|
|
152
|
+
FOR v_item IN
|
|
153
|
+
SELECT item FROM jsonb_array_elements(p_action_items) AS item
|
|
154
|
+
LOOP
|
|
155
|
+
v_description := btrim(COALESCE(v_item ->> 'description', ''));
|
|
156
|
+
IF char_length(v_description) < 5 THEN
|
|
157
|
+
RAISE WARNING 'record_meeting: skipping meeting % action item, description shorter than 5 characters (%)',
|
|
158
|
+
v_meeting_id, v_description;
|
|
159
|
+
-- Y1: the RAISE alone is not evidence. Neither meeting-service.ts nor
|
|
160
|
+
-- meetings.ts subscribes to pg's `notice` event, so a WARNING is
|
|
161
|
+
-- visible only to someone attached with psql by hand. This item
|
|
162
|
+
-- produces no row to carry its own record, so the whole submitted
|
|
163
|
+
-- item is preserved on the meeting instead — an operator can then see
|
|
164
|
+
-- that the meeting's follow-up list is incomplete, and what was lost.
|
|
165
|
+
v_skipped := v_skipped || jsonb_build_array(jsonb_build_object(
|
|
166
|
+
'reason', 'description_shorter_than_5_characters',
|
|
167
|
+
'description', v_item ->> 'description',
|
|
168
|
+
'owner', v_item ->> 'owner',
|
|
169
|
+
'target', v_item ->> 'target',
|
|
170
|
+
'assignee', v_item ->> 'assignee',
|
|
171
|
+
'deadline', v_item ->> 'deadline',
|
|
172
|
+
'priority', v_item ->> 'priority'
|
|
173
|
+
));
|
|
174
|
+
CONTINUE;
|
|
175
|
+
END IF;
|
|
176
|
+
|
|
177
|
+
-- Y1: reset per item. Anything appended here lands in the created row's
|
|
178
|
+
-- metadata under meeting_import.fallbacks, so the row itself answers
|
|
179
|
+
-- "why is this filed under semo-ops / due then / unassigned?".
|
|
180
|
+
v_fallbacks := '[]'::jsonb;
|
|
181
|
+
|
|
182
|
+
v_item_id := NULL;
|
|
183
|
+
SELECT a.action_item_id INTO v_item_id
|
|
184
|
+
FROM semicolony.action_items a
|
|
185
|
+
WHERE a.meeting_id = v_meeting_id
|
|
186
|
+
AND lower(regexp_replace(btrim(a.description), '\s+', ' ', 'g')) =
|
|
187
|
+
lower(regexp_replace(v_description, '\s+', ' ', 'g'))
|
|
188
|
+
ORDER BY a.created_at, a.action_item_id
|
|
189
|
+
LIMIT 1;
|
|
190
|
+
IF v_item_id IS NOT NULL THEN
|
|
191
|
+
v_preserved := v_preserved || jsonb_build_array(jsonb_build_object(
|
|
192
|
+
'action_item_id', v_item_id, 'submitted', v_item,
|
|
193
|
+
'reason', 'existing_followup_preserved'));
|
|
194
|
+
CONTINUE;
|
|
195
|
+
END IF;
|
|
196
|
+
|
|
197
|
+
v_priority := COALESCE(NULLIF(btrim(v_item ->> 'priority'), ''), 'normal');
|
|
198
|
+
IF v_priority NOT IN ('low', 'normal', 'high', 'urgent') THEN
|
|
199
|
+
RAISE WARNING 'record_meeting: meeting % action item priority % is not one of low/normal/high/urgent, recording as normal',
|
|
200
|
+
v_meeting_id, v_priority;
|
|
201
|
+
v_fallbacks := v_fallbacks || jsonb_build_array(jsonb_build_object(
|
|
202
|
+
'field', 'priority',
|
|
203
|
+
'reason', 'not_in_low_normal_high_urgent',
|
|
204
|
+
'substituted', 'normal',
|
|
205
|
+
'original', v_priority
|
|
206
|
+
));
|
|
207
|
+
v_priority := 'normal';
|
|
208
|
+
END IF;
|
|
209
|
+
|
|
210
|
+
v_project := NULL;
|
|
211
|
+
FOREACH v_candidate IN ARRAY ARRAY[
|
|
212
|
+
NULLIF(btrim(v_item ->> 'target'), ''),
|
|
213
|
+
NULLIF(btrim(v_item ->> 'owner'), ''),
|
|
214
|
+
NULLIF(btrim(p_target_domain), '')
|
|
215
|
+
]
|
|
216
|
+
LOOP
|
|
217
|
+
CONTINUE WHEN v_candidate IS NULL;
|
|
218
|
+
IF EXISTS (SELECT 1 FROM semicolony.ontology o
|
|
219
|
+
WHERE o.domain = v_candidate AND o.entity_type = 'service') THEN
|
|
220
|
+
v_project := v_candidate;
|
|
221
|
+
EXIT;
|
|
222
|
+
END IF;
|
|
223
|
+
END LOOP;
|
|
224
|
+
-- Y1: record the substitution before it is applied, while the rejected
|
|
225
|
+
-- candidates are still distinguishable from a deliberate 'semo-ops'.
|
|
226
|
+
IF v_project IS NULL THEN
|
|
227
|
+
RAISE WARNING 'record_meeting: meeting % action item names no entity_type=service domain (target %, owner %, meeting target %), filing under semo-ops',
|
|
228
|
+
v_meeting_id, v_item ->> 'target', v_item ->> 'owner', p_target_domain;
|
|
229
|
+
v_fallbacks := v_fallbacks || jsonb_build_array(jsonb_build_object(
|
|
230
|
+
'field', 'project_domain',
|
|
231
|
+
'reason', 'no_service_domain_among_candidates',
|
|
232
|
+
'substituted', 'semo-ops',
|
|
233
|
+
'original', jsonb_build_object(
|
|
234
|
+
'target', v_item ->> 'target',
|
|
235
|
+
'owner', v_item ->> 'owner',
|
|
236
|
+
'meeting_target_domain', p_target_domain
|
|
237
|
+
)
|
|
238
|
+
));
|
|
239
|
+
END IF;
|
|
240
|
+
v_project := COALESCE(v_project, 'semo-ops');
|
|
241
|
+
|
|
242
|
+
v_assignee_kind := 'unassigned';
|
|
243
|
+
v_assignee_member := NULL;
|
|
244
|
+
v_assignee_agent := NULL;
|
|
245
|
+
FOREACH v_candidate IN ARRAY ARRAY[
|
|
246
|
+
NULLIF(btrim(v_item ->> 'assignee'), ''),
|
|
247
|
+
NULLIF(btrim(v_item ->> 'owner'), '')
|
|
248
|
+
]
|
|
249
|
+
LOOP
|
|
250
|
+
CONTINUE WHEN v_candidate IS NULL;
|
|
251
|
+
IF EXISTS (SELECT 1 FROM semicolony.team_members tm WHERE tm.domain = v_candidate) THEN
|
|
252
|
+
v_assignee_kind := 'team-member';
|
|
253
|
+
v_assignee_member := v_candidate;
|
|
254
|
+
EXIT;
|
|
255
|
+
ELSIF EXISTS (SELECT 1 FROM semicolony.bot_status bs WHERE bs.bot_id = v_candidate) THEN
|
|
256
|
+
v_assignee_kind := 'agent';
|
|
257
|
+
v_assignee_agent := v_candidate;
|
|
258
|
+
EXIT;
|
|
259
|
+
END IF;
|
|
260
|
+
END LOOP;
|
|
261
|
+
|
|
262
|
+
v_external_contact := CASE WHEN v_assignee_kind = 'unassigned'
|
|
263
|
+
THEN NULLIF(btrim(v_item ->> 'assignee'), '')
|
|
264
|
+
ELSE NULL END;
|
|
265
|
+
|
|
266
|
+
-- Y1: only a *supplied* name that failed to resolve is a fallback. An
|
|
267
|
+
-- item that named nobody is simply unassigned, not a substitution, and
|
|
268
|
+
-- recording it as one would bury the real cases in noise.
|
|
269
|
+
IF v_assignee_kind = 'unassigned'
|
|
270
|
+
AND (NULLIF(btrim(v_item ->> 'assignee'), '') IS NOT NULL
|
|
271
|
+
OR NULLIF(btrim(v_item ->> 'owner'), '') IS NOT NULL) THEN
|
|
272
|
+
RAISE WARNING 'record_meeting: meeting % action item assignee (assignee %, owner %) is neither a team member nor an agent, leaving it unassigned',
|
|
273
|
+
v_meeting_id, v_item ->> 'assignee', v_item ->> 'owner';
|
|
274
|
+
v_fallbacks := v_fallbacks || jsonb_build_array(jsonb_build_object(
|
|
275
|
+
'field', 'assignee',
|
|
276
|
+
'reason', 'not_a_team_member_or_agent',
|
|
277
|
+
'substituted', 'unassigned',
|
|
278
|
+
'original', jsonb_build_object(
|
|
279
|
+
'assignee', v_item ->> 'assignee',
|
|
280
|
+
'owner', v_item ->> 'owner'
|
|
281
|
+
),
|
|
282
|
+
'external_contact', v_external_contact
|
|
283
|
+
));
|
|
284
|
+
END IF;
|
|
285
|
+
|
|
286
|
+
v_deadline := COALESCE(
|
|
287
|
+
NULLIF(btrim(v_item ->> 'deadline'), '')::date,
|
|
288
|
+
semicolony.action_item_default_deadline(v_priority)
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
-- Y1: the COALESCE above is deliberately left byte-identical (its shape
|
|
292
|
+
-- is pinned by the migration test); the fallback is detected separately
|
|
293
|
+
-- on the same condition rather than by restructuring it.
|
|
294
|
+
IF NULLIF(btrim(v_item ->> 'deadline'), '') IS NULL THEN
|
|
295
|
+
RAISE WARNING 'record_meeting: meeting % action item has no deadline, defaulting to % for priority %',
|
|
296
|
+
v_meeting_id, v_deadline, v_priority;
|
|
297
|
+
v_fallbacks := v_fallbacks || jsonb_build_array(jsonb_build_object(
|
|
298
|
+
'field', 'deadline',
|
|
299
|
+
'reason', 'absent',
|
|
300
|
+
'substituted', to_char(v_deadline, 'YYYY-MM-DD'),
|
|
301
|
+
'original', v_item ->> 'deadline',
|
|
302
|
+
'derived_from_priority', v_priority
|
|
303
|
+
));
|
|
304
|
+
END IF;
|
|
305
|
+
|
|
306
|
+
v_item_id := semicolony.create_action_item(
|
|
307
|
+
p_description => v_description,
|
|
308
|
+
p_project_domain => v_project,
|
|
309
|
+
p_deadline => v_deadline,
|
|
310
|
+
p_assignee_kind => v_assignee_kind,
|
|
311
|
+
p_assignee_member => v_assignee_member,
|
|
312
|
+
p_assignee_agent => v_assignee_agent,
|
|
313
|
+
p_assignee_device => NULL,
|
|
314
|
+
p_priority => v_priority,
|
|
315
|
+
p_source_kind => 'meeting',
|
|
316
|
+
p_source_ref => v_meeting_id::text,
|
|
317
|
+
p_related_url => NULL,
|
|
318
|
+
p_external_contact => v_external_contact,
|
|
319
|
+
p_metadata => jsonb_build_object(
|
|
320
|
+
'meeting_id', v_meeting_id,
|
|
321
|
+
'meeting_date', to_char(p_meeting_date, 'YYYY-MM-DD'),
|
|
322
|
+
'record_source', p_record_source,
|
|
323
|
+
'worker_id', p_worker_id,
|
|
324
|
+
'device_id', p_device_id,
|
|
325
|
+
'raw_owner', v_item ->> 'owner',
|
|
326
|
+
'raw_target', v_item ->> 'target',
|
|
327
|
+
'raw_assignee', v_item ->> 'assignee',
|
|
328
|
+
-- Y1: always present, empty array when the
|
|
329
|
+
-- item mapped cleanly, so a consumer can
|
|
330
|
+
-- filter on jsonb_array_length() without
|
|
331
|
+
-- having to distinguish absent from empty.
|
|
332
|
+
'meeting_import', jsonb_build_object(
|
|
333
|
+
'fallbacks', v_fallbacks
|
|
334
|
+
)
|
|
335
|
+
),
|
|
336
|
+
p_origin_surface => 'system',
|
|
337
|
+
p_origin_actor_kind => NULL,
|
|
338
|
+
p_origin_member => NULL,
|
|
339
|
+
p_origin_agent => NULL,
|
|
340
|
+
p_origin_device => NULL,
|
|
341
|
+
p_origin_harness => NULL,
|
|
342
|
+
p_origin_session_key => NULL
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
-- The typed relation 198 added. create_action_item deliberately has no
|
|
346
|
+
-- meeting_id parameter: meetings are one source among nine and the
|
|
347
|
+
-- function's parameter list is already twenty wide, so the linkage is
|
|
348
|
+
-- stamped here, in the same transaction, rather than widening every
|
|
349
|
+
-- caller's signature for one of them.
|
|
350
|
+
UPDATE semicolony.action_items
|
|
351
|
+
SET meeting_id = v_meeting_id
|
|
352
|
+
WHERE action_item_id = v_item_id;
|
|
353
|
+
END LOOP;
|
|
354
|
+
|
|
355
|
+
-- Y1: publish the skip list on the meeting, inside the same IF that owns
|
|
356
|
+
-- the delete-and-recreate. `||` replaces the whole meeting_import key
|
|
357
|
+
-- rather than merging into it, so a re-record with the descriptions fixed
|
|
358
|
+
-- writes back an empty array instead of leaving a stale accusation on a
|
|
359
|
+
-- meeting whose follow-up list is now complete — the evidence follows the
|
|
360
|
+
-- same idempotency rule as the items it describes. Keeping it inside the
|
|
361
|
+
-- IF also means an empty submission leaves both the items and their
|
|
362
|
+
-- evidence untouched, which is the behaviour 199 defined and verification
|
|
363
|
+
-- confirmed.
|
|
364
|
+
UPDATE semicolony.meetings m
|
|
365
|
+
SET metadata = COALESCE(m.metadata, '{}'::jsonb)
|
|
366
|
+
|| jsonb_build_object('meeting_import', jsonb_build_object(
|
|
367
|
+
'preserved_action_items', v_preserved,
|
|
368
|
+
'preserved_count', jsonb_array_length(v_preserved),
|
|
369
|
+
'skipped_action_items', v_skipped,
|
|
370
|
+
'skipped_count', jsonb_array_length(v_skipped),
|
|
371
|
+
'submitted_count', jsonb_array_length(p_action_items),
|
|
372
|
+
'recorded_at', now(),
|
|
373
|
+
'record_source', p_record_source
|
|
374
|
+
)),
|
|
375
|
+
updated_at = now()
|
|
376
|
+
WHERE m.meeting_id = v_meeting_id;
|
|
377
|
+
END IF;
|
|
378
|
+
|
|
379
|
+
RETURN QUERY
|
|
380
|
+
SELECT v_meeting_id,
|
|
381
|
+
v_kb_id,
|
|
382
|
+
COALESCE(ARRAY(SELECT a.action_item_id
|
|
383
|
+
FROM semicolony.action_items a
|
|
384
|
+
WHERE a.meeting_id = v_meeting_id
|
|
385
|
+
ORDER BY a.created_at), '{}'::uuid[]),
|
|
386
|
+
(v_existing IS NOT NULL);
|
|
387
|
+
END;
|
|
388
|
+
$function$;
|