@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.
- package/dist/bundle.js +1195 -1177
- package/migrations/198_meeting_record_canonical.sql +85 -0
- package/migrations/199_meeting_record_capability.sql +164 -0
- package/migrations/200_meeting_credential_scope.sql +320 -0
- package/migrations/201_meeting_audio_data_drop.sql +61 -0
- package/migrations/202_meeting_record_discussion_provenance.sql +173 -0
- package/migrations/202_team_members_record_of_authority.sql +103 -0
- package/migrations/203_action_item_redesign_cutover.sql +1369 -0
- package/migrations/204_record_meeting_overload_repair.sql +463 -0
- package/migrations/205_action_item_credential_scope.sql +318 -0
- package/package.json +1 -1
|
@@ -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
|
+
);
|