@team-semicolon/semicolony-cli 4.18.104 → 4.18.106

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,463 @@
1
+ -- 204_record_meeting_overload_repair.sql
2
+ --
3
+ -- Two `record_meeting` overloads exist on the live operator database and every
4
+ -- meeting recording is failing. Measured 2026-08-13 against production:
5
+ --
6
+ -- semo_ops.record_meeting(text,date,text,text,text,text,jsonb,text,text,jsonb,text,text,text)
7
+ -- semo_ops.record_meeting(text,date,text,text,text,text,jsonb,text,text,jsonb,text,text,text,text,integer)
8
+ --
9
+ -- How they came to coexist: 202_meeting_record_discussion_provenance.sql (dev)
10
+ -- dropped the 13-argument signature and created the 15-argument one, adding
11
+ -- p_discussion_url/p_discussion_number with DEFAULT NULL. Forty-nine minutes
12
+ -- later 203_action_item_redesign_cutover.sql ran `CREATE OR REPLACE` on the
13
+ -- 13-argument signature -- written before that branch existed, and correct in
14
+ -- isolation -- which recreated the overload the earlier migration had removed.
15
+ -- The same collision reproduces on a fresh install, because the runner applies
16
+ -- these files in filename order (202_meeting... < 202_team... < 203) and 203
17
+ -- therefore always lands last.
18
+ --
19
+ -- Both live callers -- packages/kb-gateway/src/lib/meeting-service.ts:45 and
20
+ -- packages/cli/src/commands/meetings.ts:181 -- pass exactly 13 positional
21
+ -- parameters, and node-pg sends them untyped. Because the 15-argument version's
22
+ -- last two parameters are defaulted, a 13-argument call is a legal candidate for
23
+ -- both, and PostgreSQL refuses to choose:
24
+ --
25
+ -- ERROR: function semo_ops.record_meeting(unknown, unknown, ..., jsonb, ...)
26
+ -- is not unique
27
+ -- HINT: Could not choose a best candidate function.
28
+ --
29
+ -- Reproduced 2026-08-13 on a throwaway PostgreSQL 16.14 loaded from the
30
+ -- production schema dump with 202_meeting_record_discussion_provenance,
31
+ -- 202_team_members_record_of_authority and 203 applied in the live order.
32
+ --
33
+ -- Why one function with the 15-argument signature, and not the other way round:
34
+ --
35
+ -- * The 15-argument signature is the newer canonical one and carries real
36
+ -- data -- discussion_url/discussion_number are how a migrated record points
37
+ -- back at the Discussion it was built from, which Phase 2's backfill of 205
38
+ -- Meeting-Minutes Discussions depends on. Dropping it to keep the narrower
39
+ -- signature would delete a capability.
40
+ -- * Its body, however, still writes the pre-redesign action-item shape:
41
+ -- INSERT INTO action_items (owner_domain, target_domain, description,
42
+ -- assignee, deadline, priority, status, source, meeting_id, ...) -- eight
43
+ -- columns 203 removed. Dropping only *our* 13-argument overload would
44
+ -- restore unambiguous resolution and leave meeting recording broken a
45
+ -- different way, at first execution rather than at migration time (a
46
+ -- PL/pgSQL body's column references resolve on first call, not at CREATE).
47
+ --
48
+ -- So: the 15-argument signature and everything it owns -- the validations, the
49
+ -- meetings upsert including the discussion-provenance columns, the Knowledge
50
+ -- Fabric projection with its restricted-classification metadata, the
51
+ -- delete-and-recreate idempotency, and the four-column receipt -- are carried
52
+ -- over unchanged from 202_meeting_record_discussion_provenance. The action-item
53
+ -- half is carried over unchanged from 203: create_action_item, the typed
54
+ -- meeting_id stamp, source_kind='meeting' with source_ref, the
55
+ -- project/assignee/deadline/priority fallbacks, and the meeting_import evidence
56
+ -- recorded in action_items.metadata and meetings.metadata. Neither half is
57
+ -- redesigned here; this file only puts them in the same function.
58
+ --
59
+ -- Statement order is load-bearing. The replace comes first, so the 15-argument
60
+ -- function is already correct before the 13-argument one goes away; there is
61
+ -- never a moment with no working record_meeting. (The runner wraps the file in
62
+ -- one transaction, so the intermediate state is not observable to anyone else
63
+ -- either -- but the order is still written to be correct without that.)
64
+ --
65
+ -- The runner owns the transaction boundary, so no BEGIN/COMMIT here, and it
66
+ -- retargets the `semicolony.` qualifier to the active schema.
67
+
68
+ CREATE OR REPLACE FUNCTION semicolony.record_meeting(
69
+ p_title text,
70
+ p_meeting_date date,
71
+ p_meeting_type text,
72
+ p_adhoc_subtype text,
73
+ p_visibility text,
74
+ p_target_domain text,
75
+ p_attendees jsonb,
76
+ p_record_body text,
77
+ p_transcript text,
78
+ p_action_items jsonb,
79
+ p_record_source text,
80
+ p_worker_id text,
81
+ p_device_id text,
82
+ p_discussion_url text DEFAULT NULL,
83
+ p_discussion_number integer DEFAULT NULL
84
+ )
85
+ RETURNS TABLE(meeting_id uuid, kb_id bigint, action_item_ids uuid[], duplicate boolean)
86
+ LANGUAGE plpgsql
87
+ SECURITY DEFINER
88
+ SET search_path = pg_catalog
89
+ AS $function$
90
+ DECLARE
91
+ v_meeting_id uuid;
92
+ v_kb_id bigint;
93
+ v_existing uuid;
94
+ v_norm text;
95
+ v_sensitivity text;
96
+ v_sub_key text;
97
+ v_item jsonb;
98
+ v_item_id uuid;
99
+ v_description text;
100
+ v_priority text;
101
+ v_project text;
102
+ v_candidate text;
103
+ v_assignee_kind text;
104
+ v_assignee_member text;
105
+ v_assignee_agent text;
106
+ v_external_contact text;
107
+ v_deadline date;
108
+ -- Y1: fallbacks are recorded as data, not only as a log line. v_fallbacks
109
+ -- accumulates per item and lands in that item's own metadata; v_skipped
110
+ -- accumulates items that produced no row at all and lands on the meeting.
111
+ v_fallbacks jsonb;
112
+ v_skipped jsonb;
113
+ BEGIN
114
+ IF p_title IS NULL OR length(btrim(p_title)) < 3 THEN
115
+ RAISE EXCEPTION 'title must be at least 3 characters';
116
+ END IF;
117
+ IF p_meeting_date IS NULL THEN
118
+ RAISE EXCEPTION 'meeting_date is required';
119
+ END IF;
120
+ IF p_visibility IS NULL OR p_visibility NOT IN ('team', 'restricted') THEN
121
+ RAISE EXCEPTION 'visibility must be team or restricted, got %', p_visibility;
122
+ END IF;
123
+ IF p_record_source IS NULL
124
+ OR p_record_source NOT IN ('slack-worker', 'operator', 'backfill-discussion') THEN
125
+ RAISE EXCEPTION 'invalid record_source %', p_record_source;
126
+ END IF;
127
+ IF p_record_body IS NULL OR length(btrim(p_record_body)) = 0 THEN
128
+ RAISE EXCEPTION 'record_body is required';
129
+ END IF;
130
+
131
+ v_norm := lower(regexp_replace(btrim(p_title), '\s+', ' ', 'g'));
132
+ v_sensitivity := CASE WHEN p_visibility = 'restricted' THEN 'restricted' ELSE 'internal' END;
133
+
134
+ SELECT m.meeting_id INTO v_existing
135
+ FROM semicolony.meetings m
136
+ WHERE m.meeting_date = p_meeting_date
137
+ AND lower(regexp_replace(m.title, '\s+', ' ', 'g')) = v_norm;
138
+
139
+ -- Discussion provenance from 202_meeting_record_discussion_provenance,
140
+ -- unchanged: COALESCE on update so a re-record that omits the link does not
141
+ -- erase one an earlier submission established, and both columns written on
142
+ -- insert.
143
+ IF v_existing IS NOT NULL THEN
144
+ UPDATE semicolony.meetings m
145
+ SET record_body = COALESCE(p_record_body, m.record_body),
146
+ mapped_transcript = COALESCE(p_transcript, m.mapped_transcript),
147
+ visibility = p_visibility,
148
+ record_source = p_record_source,
149
+ target_domain = COALESCE(p_target_domain, m.target_domain),
150
+ attendees = COALESCE(p_attendees, m.attendees),
151
+ discussion_url = COALESCE(p_discussion_url, m.discussion_url),
152
+ discussion_number = COALESCE(p_discussion_number, m.discussion_number),
153
+ updated_at = now()
154
+ WHERE m.meeting_id = v_existing;
155
+ v_meeting_id := v_existing;
156
+ ELSE
157
+ INSERT INTO semicolony.meetings (
158
+ meeting_id, title, meeting_type, adhoc_subtype, meeting_date, attendees,
159
+ visibility, record_body, mapped_transcript, record_source, target_domain,
160
+ transcription_status, speaker_map, discussion_url, discussion_number,
161
+ created_at, updated_at
162
+ ) VALUES (
163
+ gen_random_uuid(), btrim(p_title), p_meeting_type, p_adhoc_subtype, p_meeting_date,
164
+ COALESCE(p_attendees, '[]'::jsonb), p_visibility, p_record_body,
165
+ p_transcript, p_record_source, p_target_domain,
166
+ 'completed', '{}'::jsonb, p_discussion_url, p_discussion_number, now(), now()
167
+ ) RETURNING meetings.meeting_id INTO v_meeting_id;
168
+ END IF;
169
+
170
+ v_sub_key := to_char(p_meeting_date, 'YYYY-MM-DD') || '-' || left(v_norm, 60);
171
+
172
+ INSERT INTO semicolony.knowledge_base (
173
+ domain, key, sub_key, content, metadata, created_by, created_at, updated_at
174
+ ) VALUES (
175
+ COALESCE(p_target_domain, 'semicolon'),
176
+ 'meeting',
177
+ v_sub_key,
178
+ p_record_body,
179
+ jsonb_build_object(
180
+ 'classification', jsonb_build_object('sensitivity', v_sensitivity),
181
+ 'meeting_id', v_meeting_id,
182
+ 'meeting_date', to_char(p_meeting_date, 'YYYY-MM-DD'),
183
+ 'record_source', p_record_source
184
+ ),
185
+ COALESCE(p_worker_id, 'meeting-worker'),
186
+ now(), now()
187
+ )
188
+ ON CONFLICT (domain, key, sub_key) DO UPDATE
189
+ SET content = EXCLUDED.content,
190
+ metadata = semicolony.knowledge_base.metadata || EXCLUDED.metadata,
191
+ updated_at = now()
192
+ RETURNING semicolony.knowledge_base.kb_id INTO v_kb_id;
193
+
194
+ -- Re-recording the same meeting must not duplicate its follow-ups. Existing
195
+ -- machine-created items for this meeting are replaced; anything a human filed
196
+ -- separately carries no meeting_id and is untouched. Unchanged from 199,
197
+ -- including the fact that the DELETE only runs when the caller actually sent
198
+ -- items -- a record submitted with an empty action-item list must not silently
199
+ -- erase the follow-ups an earlier submission of the same meeting created.
200
+ -- action_item_events rows disappear with their item through the events
201
+ -- table's ON DELETE CASCADE, so no orphan history is left behind.
202
+ IF p_action_items IS NOT NULL AND jsonb_typeof(p_action_items) = 'array'
203
+ AND jsonb_array_length(p_action_items) > 0 THEN
204
+ DELETE FROM semicolony.action_items a WHERE a.meeting_id = v_meeting_id;
205
+
206
+ v_skipped := '[]'::jsonb;
207
+
208
+ FOR v_item IN
209
+ SELECT item FROM jsonb_array_elements(p_action_items) AS item
210
+ LOOP
211
+ v_description := btrim(COALESCE(v_item ->> 'description', ''));
212
+ IF char_length(v_description) < 5 THEN
213
+ RAISE WARNING 'record_meeting: skipping meeting % action item, description shorter than 5 characters (%)',
214
+ v_meeting_id, v_description;
215
+ -- Y1: the RAISE alone is not evidence. Neither meeting-service.ts nor
216
+ -- meetings.ts subscribes to pg's `notice` event, so a WARNING is
217
+ -- visible only to someone attached with psql by hand. This item
218
+ -- produces no row to carry its own record, so the whole submitted
219
+ -- item is preserved on the meeting instead — an operator can then see
220
+ -- that the meeting's follow-up list is incomplete, and what was lost.
221
+ v_skipped := v_skipped || jsonb_build_array(jsonb_build_object(
222
+ 'reason', 'description_shorter_than_5_characters',
223
+ 'description', v_item ->> 'description',
224
+ 'owner', v_item ->> 'owner',
225
+ 'target', v_item ->> 'target',
226
+ 'assignee', v_item ->> 'assignee',
227
+ 'deadline', v_item ->> 'deadline',
228
+ 'priority', v_item ->> 'priority'
229
+ ));
230
+ CONTINUE;
231
+ END IF;
232
+
233
+ -- Y1: reset per item. Anything appended here lands in the created row's
234
+ -- metadata under meeting_import.fallbacks, so the row itself answers
235
+ -- "why is this filed under semo-ops / due then / unassigned?".
236
+ v_fallbacks := '[]'::jsonb;
237
+
238
+ v_priority := COALESCE(NULLIF(btrim(v_item ->> 'priority'), ''), 'normal');
239
+ IF v_priority NOT IN ('low', 'normal', 'high', 'urgent') THEN
240
+ RAISE WARNING 'record_meeting: meeting % action item priority % is not one of low/normal/high/urgent, recording as normal',
241
+ v_meeting_id, v_priority;
242
+ v_fallbacks := v_fallbacks || jsonb_build_array(jsonb_build_object(
243
+ 'field', 'priority',
244
+ 'reason', 'not_in_low_normal_high_urgent',
245
+ 'substituted', 'normal',
246
+ 'original', v_priority
247
+ ));
248
+ v_priority := 'normal';
249
+ END IF;
250
+
251
+ v_project := NULL;
252
+ FOREACH v_candidate IN ARRAY ARRAY[
253
+ NULLIF(btrim(v_item ->> 'target'), ''),
254
+ NULLIF(btrim(v_item ->> 'owner'), ''),
255
+ NULLIF(btrim(p_target_domain), '')
256
+ ]
257
+ LOOP
258
+ CONTINUE WHEN v_candidate IS NULL;
259
+ IF EXISTS (SELECT 1 FROM semicolony.ontology o
260
+ WHERE o.domain = v_candidate AND o.entity_type = 'service') THEN
261
+ v_project := v_candidate;
262
+ EXIT;
263
+ END IF;
264
+ END LOOP;
265
+ -- Y1: record the substitution before it is applied, while the rejected
266
+ -- candidates are still distinguishable from a deliberate 'semo-ops'.
267
+ IF v_project IS NULL THEN
268
+ RAISE WARNING 'record_meeting: meeting % action item names no entity_type=service domain (target %, owner %, meeting target %), filing under semo-ops',
269
+ v_meeting_id, v_item ->> 'target', v_item ->> 'owner', p_target_domain;
270
+ v_fallbacks := v_fallbacks || jsonb_build_array(jsonb_build_object(
271
+ 'field', 'project_domain',
272
+ 'reason', 'no_service_domain_among_candidates',
273
+ 'substituted', 'semo-ops',
274
+ 'original', jsonb_build_object(
275
+ 'target', v_item ->> 'target',
276
+ 'owner', v_item ->> 'owner',
277
+ 'meeting_target_domain', p_target_domain
278
+ )
279
+ ));
280
+ END IF;
281
+ v_project := COALESCE(v_project, 'semo-ops');
282
+
283
+ v_assignee_kind := 'unassigned';
284
+ v_assignee_member := NULL;
285
+ v_assignee_agent := NULL;
286
+ FOREACH v_candidate IN ARRAY ARRAY[
287
+ NULLIF(btrim(v_item ->> 'assignee'), ''),
288
+ NULLIF(btrim(v_item ->> 'owner'), '')
289
+ ]
290
+ LOOP
291
+ CONTINUE WHEN v_candidate IS NULL;
292
+ IF EXISTS (SELECT 1 FROM semicolony.team_members tm WHERE tm.domain = v_candidate) THEN
293
+ v_assignee_kind := 'team-member';
294
+ v_assignee_member := v_candidate;
295
+ EXIT;
296
+ ELSIF EXISTS (SELECT 1 FROM semicolony.bot_status bs WHERE bs.bot_id = v_candidate) THEN
297
+ v_assignee_kind := 'agent';
298
+ v_assignee_agent := v_candidate;
299
+ EXIT;
300
+ END IF;
301
+ END LOOP;
302
+
303
+ v_external_contact := CASE WHEN v_assignee_kind = 'unassigned'
304
+ THEN NULLIF(btrim(v_item ->> 'assignee'), '')
305
+ ELSE NULL END;
306
+
307
+ -- Y1: only a *supplied* name that failed to resolve is a fallback. An
308
+ -- item that named nobody is simply unassigned, not a substitution, and
309
+ -- recording it as one would bury the real cases in noise.
310
+ IF v_assignee_kind = 'unassigned'
311
+ AND (NULLIF(btrim(v_item ->> 'assignee'), '') IS NOT NULL
312
+ OR NULLIF(btrim(v_item ->> 'owner'), '') IS NOT NULL) THEN
313
+ RAISE WARNING 'record_meeting: meeting % action item assignee (assignee %, owner %) is neither a team member nor an agent, leaving it unassigned',
314
+ v_meeting_id, v_item ->> 'assignee', v_item ->> 'owner';
315
+ v_fallbacks := v_fallbacks || jsonb_build_array(jsonb_build_object(
316
+ 'field', 'assignee',
317
+ 'reason', 'not_a_team_member_or_agent',
318
+ 'substituted', 'unassigned',
319
+ 'original', jsonb_build_object(
320
+ 'assignee', v_item ->> 'assignee',
321
+ 'owner', v_item ->> 'owner'
322
+ ),
323
+ 'external_contact', v_external_contact
324
+ ));
325
+ END IF;
326
+
327
+ v_deadline := COALESCE(
328
+ NULLIF(btrim(v_item ->> 'deadline'), '')::date,
329
+ semicolony.action_item_default_deadline(v_priority)
330
+ );
331
+
332
+ -- Y1: the COALESCE above is deliberately left byte-identical (its shape
333
+ -- is pinned by the migration test); the fallback is detected separately
334
+ -- on the same condition rather than by restructuring it.
335
+ IF NULLIF(btrim(v_item ->> 'deadline'), '') IS NULL THEN
336
+ RAISE WARNING 'record_meeting: meeting % action item has no deadline, defaulting to % for priority %',
337
+ v_meeting_id, v_deadline, v_priority;
338
+ v_fallbacks := v_fallbacks || jsonb_build_array(jsonb_build_object(
339
+ 'field', 'deadline',
340
+ 'reason', 'absent',
341
+ 'substituted', to_char(v_deadline, 'YYYY-MM-DD'),
342
+ 'original', v_item ->> 'deadline',
343
+ 'derived_from_priority', v_priority
344
+ ));
345
+ END IF;
346
+
347
+ v_item_id := semicolony.create_action_item(
348
+ p_description => v_description,
349
+ p_project_domain => v_project,
350
+ p_deadline => v_deadline,
351
+ p_assignee_kind => v_assignee_kind,
352
+ p_assignee_member => v_assignee_member,
353
+ p_assignee_agent => v_assignee_agent,
354
+ p_assignee_device => NULL,
355
+ p_priority => v_priority,
356
+ p_source_kind => 'meeting',
357
+ p_source_ref => v_meeting_id::text,
358
+ p_related_url => NULL,
359
+ p_external_contact => v_external_contact,
360
+ p_metadata => jsonb_build_object(
361
+ 'meeting_id', v_meeting_id,
362
+ 'meeting_date', to_char(p_meeting_date, 'YYYY-MM-DD'),
363
+ 'record_source', p_record_source,
364
+ 'worker_id', p_worker_id,
365
+ 'device_id', p_device_id,
366
+ 'raw_owner', v_item ->> 'owner',
367
+ 'raw_target', v_item ->> 'target',
368
+ 'raw_assignee', v_item ->> 'assignee',
369
+ -- Y1: always present, empty array when the
370
+ -- item mapped cleanly, so a consumer can
371
+ -- filter on jsonb_array_length() without
372
+ -- having to distinguish absent from empty.
373
+ 'meeting_import', jsonb_build_object(
374
+ 'fallbacks', v_fallbacks
375
+ )
376
+ ),
377
+ p_origin_surface => 'system',
378
+ p_origin_actor_kind => NULL,
379
+ p_origin_member => NULL,
380
+ p_origin_agent => NULL,
381
+ p_origin_device => NULL,
382
+ p_origin_harness => NULL,
383
+ p_origin_session_key => NULL
384
+ );
385
+
386
+ -- The typed relation 198 added. create_action_item deliberately has no
387
+ -- meeting_id parameter: meetings are one source among nine and the
388
+ -- function's parameter list is already twenty wide, so the linkage is
389
+ -- stamped here, in the same transaction, rather than widening every
390
+ -- caller's signature for one of them.
391
+ UPDATE semicolony.action_items
392
+ SET meeting_id = v_meeting_id
393
+ WHERE action_item_id = v_item_id;
394
+ END LOOP;
395
+
396
+ -- Y1: publish the skip list on the meeting, inside the same IF that owns
397
+ -- the delete-and-recreate. `||` replaces the whole meeting_import key
398
+ -- rather than merging into it, so a re-record with the descriptions fixed
399
+ -- writes back an empty array instead of leaving a stale accusation on a
400
+ -- meeting whose follow-up list is now complete — the evidence follows the
401
+ -- same idempotency rule as the items it describes. Keeping it inside the
402
+ -- IF also means an empty submission leaves both the items and their
403
+ -- evidence untouched, which is the behaviour 199 defined and verification
404
+ -- confirmed.
405
+ UPDATE semicolony.meetings m
406
+ SET metadata = COALESCE(m.metadata, '{}'::jsonb)
407
+ || jsonb_build_object('meeting_import', jsonb_build_object(
408
+ 'skipped_action_items', v_skipped,
409
+ 'skipped_count', jsonb_array_length(v_skipped),
410
+ 'submitted_count', jsonb_array_length(p_action_items),
411
+ 'recorded_at', now(),
412
+ 'record_source', p_record_source
413
+ )),
414
+ updated_at = now()
415
+ WHERE m.meeting_id = v_meeting_id;
416
+ END IF;
417
+
418
+ RETURN QUERY
419
+ SELECT v_meeting_id,
420
+ v_kb_id,
421
+ COALESCE(ARRAY(SELECT a.action_item_id
422
+ FROM semicolony.action_items a
423
+ WHERE a.meeting_id = v_meeting_id
424
+ ORDER BY a.created_at), '{}'::uuid[]),
425
+ (v_existing IS NOT NULL);
426
+ END;
427
+ $function$;
428
+
429
+ -- Restated from 202_meeting_record_discussion_provenance unchanged.
430
+ --
431
+ -- CREATE OR REPLACE preserves the existing ACL: pg_proc.proacl belongs to the
432
+ -- pg_proc row, and REPLACE rewrites that row's prosrc/proargtypes without
433
+ -- touching proacl (only CREATE of a genuinely new function initialises it to
434
+ -- NULL, meaning owner-plus-PUBLIC default). The signature above is identical to
435
+ -- the one 202_meeting_record_discussion_provenance created, so this is a
436
+ -- replace and not a create, and the {=X/…,sc_app=X/…} entry that migration left
437
+ -- in proacl survives -- confirmed on the reproduction by reading
438
+ -- information_schema.routine_privileges before and after applying this file.
439
+ -- They are restated anyway for the same reason 203 restated them: so a fresh
440
+ -- install ending at this file has the identical grant surface, and so the
441
+ -- sc_app contract (EXECUTE only, still no DML anywhere) is legible in the file
442
+ -- that now owns the function body.
443
+ REVOKE ALL ON FUNCTION semicolony.record_meeting(
444
+ text, date, text, text, text, text, jsonb, text, text, jsonb, text, text, text, text, integer
445
+ ) FROM PUBLIC;
446
+
447
+ GRANT EXECUTE ON FUNCTION semicolony.record_meeting(
448
+ text, date, text, text, text, text, jsonb, text, text, jsonb, text, text, text, text, integer
449
+ ) TO sc_app;
450
+
451
+ -- The overload goes away only now that its replacement is in place.
452
+ --
453
+ -- The type list is the full 13, spelled out: DROP FUNCTION matches on the
454
+ -- declared argument types exactly and does not consider defaults, so this
455
+ -- names the 13-argument function and cannot reach the 15-argument one above --
456
+ -- which is why it is safe to write here at all, and why omitting the list (or
457
+ -- writing a shorter prefix of it) would instead raise `function name is not
458
+ -- unique` and leave both overloads in place. IF EXISTS so that re-running the
459
+ -- file, or a fresh install where some future migration has already retired the
460
+ -- narrow signature, is not an error.
461
+ DROP FUNCTION IF EXISTS semicolony.record_meeting(
462
+ text, date, text, text, text, text, jsonb, text, text, jsonb, text, text, text
463
+ );