@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,173 @@
|
|
|
1
|
+
-- 202_meeting_record_discussion_provenance.sql
|
|
2
|
+
--
|
|
3
|
+
-- Let `record_meeting` persist where a record came from.
|
|
4
|
+
--
|
|
5
|
+
-- 198 kept `discussion_url` and `discussion_number` precisely so a migrated
|
|
6
|
+
-- record could point back at the Discussion it was built from, but 199 had no
|
|
7
|
+
-- way to set them. Phase 2 backfills 205 Meeting-Minutes Discussions and needs
|
|
8
|
+
-- that link — without it a migrated record cannot be checked against its source.
|
|
9
|
+
--
|
|
10
|
+
-- Both parameters default to NULL, so every existing caller (the gateway route,
|
|
11
|
+
-- the CLI, the skills) keeps working unchanged.
|
|
12
|
+
--
|
|
13
|
+
-- The 13-argument signature is dropped first. Adding defaulted parameters
|
|
14
|
+
-- creates an overload rather than replacing, and two overloads would make every
|
|
15
|
+
-- existing call ambiguous.
|
|
16
|
+
--
|
|
17
|
+
-- The runner owns the transaction boundary, so no BEGIN/COMMIT here, and it
|
|
18
|
+
-- retargets the `semicolony.` qualifier to the active schema.
|
|
19
|
+
|
|
20
|
+
DROP FUNCTION IF EXISTS semicolony.record_meeting(
|
|
21
|
+
text, date, text, text, text, text, jsonb, text, text, jsonb, text, text, text
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
CREATE OR REPLACE FUNCTION semicolony.record_meeting(
|
|
25
|
+
p_title text,
|
|
26
|
+
p_meeting_date date,
|
|
27
|
+
p_meeting_type text,
|
|
28
|
+
p_adhoc_subtype text,
|
|
29
|
+
p_visibility text,
|
|
30
|
+
p_target_domain text,
|
|
31
|
+
p_attendees jsonb,
|
|
32
|
+
p_record_body text,
|
|
33
|
+
p_transcript text,
|
|
34
|
+
p_action_items jsonb,
|
|
35
|
+
p_record_source text,
|
|
36
|
+
p_worker_id text,
|
|
37
|
+
p_device_id text,
|
|
38
|
+
p_discussion_url text DEFAULT NULL,
|
|
39
|
+
p_discussion_number integer DEFAULT NULL
|
|
40
|
+
)
|
|
41
|
+
RETURNS TABLE(meeting_id uuid, kb_id bigint, action_item_ids uuid[], duplicate boolean)
|
|
42
|
+
LANGUAGE plpgsql
|
|
43
|
+
SECURITY DEFINER
|
|
44
|
+
SET search_path = pg_catalog
|
|
45
|
+
AS $function$
|
|
46
|
+
DECLARE
|
|
47
|
+
v_meeting_id uuid;
|
|
48
|
+
v_kb_id bigint;
|
|
49
|
+
v_existing uuid;
|
|
50
|
+
v_norm text;
|
|
51
|
+
v_sensitivity text;
|
|
52
|
+
v_sub_key text;
|
|
53
|
+
BEGIN
|
|
54
|
+
IF p_title IS NULL OR length(btrim(p_title)) < 3 THEN
|
|
55
|
+
RAISE EXCEPTION 'title must be at least 3 characters';
|
|
56
|
+
END IF;
|
|
57
|
+
IF p_meeting_date IS NULL THEN
|
|
58
|
+
RAISE EXCEPTION 'meeting_date is required';
|
|
59
|
+
END IF;
|
|
60
|
+
IF p_visibility IS NULL OR p_visibility NOT IN ('team', 'restricted') THEN
|
|
61
|
+
RAISE EXCEPTION 'visibility must be team or restricted, got %', p_visibility;
|
|
62
|
+
END IF;
|
|
63
|
+
IF p_record_source IS NULL
|
|
64
|
+
OR p_record_source NOT IN ('slack-worker', 'operator', 'backfill-discussion') THEN
|
|
65
|
+
RAISE EXCEPTION 'invalid record_source %', p_record_source;
|
|
66
|
+
END IF;
|
|
67
|
+
IF p_record_body IS NULL OR length(btrim(p_record_body)) = 0 THEN
|
|
68
|
+
RAISE EXCEPTION 'record_body is required';
|
|
69
|
+
END IF;
|
|
70
|
+
|
|
71
|
+
v_norm := lower(regexp_replace(btrim(p_title), '\s+', ' ', 'g'));
|
|
72
|
+
v_sensitivity := CASE WHEN p_visibility = 'restricted' THEN 'restricted' ELSE 'internal' END;
|
|
73
|
+
|
|
74
|
+
SELECT m.meeting_id INTO v_existing
|
|
75
|
+
FROM semicolony.meetings m
|
|
76
|
+
WHERE m.meeting_date = p_meeting_date
|
|
77
|
+
AND lower(regexp_replace(m.title, '\s+', ' ', 'g')) = v_norm;
|
|
78
|
+
|
|
79
|
+
IF v_existing IS NOT NULL THEN
|
|
80
|
+
UPDATE semicolony.meetings m
|
|
81
|
+
SET record_body = COALESCE(p_record_body, m.record_body),
|
|
82
|
+
mapped_transcript = COALESCE(p_transcript, m.mapped_transcript),
|
|
83
|
+
visibility = p_visibility,
|
|
84
|
+
record_source = p_record_source,
|
|
85
|
+
target_domain = COALESCE(p_target_domain, m.target_domain),
|
|
86
|
+
attendees = COALESCE(p_attendees, m.attendees),
|
|
87
|
+
discussion_url = COALESCE(p_discussion_url, m.discussion_url),
|
|
88
|
+
discussion_number = COALESCE(p_discussion_number, m.discussion_number),
|
|
89
|
+
updated_at = now()
|
|
90
|
+
WHERE m.meeting_id = v_existing;
|
|
91
|
+
v_meeting_id := v_existing;
|
|
92
|
+
ELSE
|
|
93
|
+
INSERT INTO semicolony.meetings (
|
|
94
|
+
meeting_id, title, meeting_type, adhoc_subtype, meeting_date, attendees,
|
|
95
|
+
visibility, record_body, mapped_transcript, record_source, target_domain,
|
|
96
|
+
transcription_status, speaker_map, discussion_url, discussion_number,
|
|
97
|
+
created_at, updated_at
|
|
98
|
+
) VALUES (
|
|
99
|
+
gen_random_uuid(), btrim(p_title), p_meeting_type, p_adhoc_subtype, p_meeting_date,
|
|
100
|
+
COALESCE(p_attendees, '[]'::jsonb), p_visibility, p_record_body,
|
|
101
|
+
p_transcript, p_record_source, p_target_domain,
|
|
102
|
+
'completed', '{}'::jsonb, p_discussion_url, p_discussion_number, now(), now()
|
|
103
|
+
) RETURNING meetings.meeting_id INTO v_meeting_id;
|
|
104
|
+
END IF;
|
|
105
|
+
|
|
106
|
+
v_sub_key := to_char(p_meeting_date, 'YYYY-MM-DD') || '-' || left(v_norm, 60);
|
|
107
|
+
|
|
108
|
+
INSERT INTO semicolony.knowledge_base (
|
|
109
|
+
domain, key, sub_key, content, metadata, created_by, created_at, updated_at
|
|
110
|
+
) VALUES (
|
|
111
|
+
COALESCE(p_target_domain, 'semicolon'),
|
|
112
|
+
'meeting',
|
|
113
|
+
v_sub_key,
|
|
114
|
+
p_record_body,
|
|
115
|
+
jsonb_build_object(
|
|
116
|
+
'classification', jsonb_build_object('sensitivity', v_sensitivity),
|
|
117
|
+
'meeting_id', v_meeting_id,
|
|
118
|
+
'meeting_date', to_char(p_meeting_date, 'YYYY-MM-DD'),
|
|
119
|
+
'record_source', p_record_source
|
|
120
|
+
),
|
|
121
|
+
COALESCE(p_worker_id, 'meeting-worker'),
|
|
122
|
+
now(), now()
|
|
123
|
+
)
|
|
124
|
+
ON CONFLICT (domain, key, sub_key) DO UPDATE
|
|
125
|
+
SET content = EXCLUDED.content,
|
|
126
|
+
metadata = semicolony.knowledge_base.metadata || EXCLUDED.metadata,
|
|
127
|
+
updated_at = now()
|
|
128
|
+
RETURNING semicolony.knowledge_base.kb_id INTO v_kb_id;
|
|
129
|
+
|
|
130
|
+
-- Re-recording the same meeting must not duplicate its follow-ups. Existing
|
|
131
|
+
-- machine-created items for this meeting are replaced; anything a human filed
|
|
132
|
+
-- separately carries no meeting_id and is untouched.
|
|
133
|
+
IF p_action_items IS NOT NULL AND jsonb_typeof(p_action_items) = 'array'
|
|
134
|
+
AND jsonb_array_length(p_action_items) > 0 THEN
|
|
135
|
+
DELETE FROM semicolony.action_items a WHERE a.meeting_id = v_meeting_id;
|
|
136
|
+
|
|
137
|
+
INSERT INTO semicolony.action_items (
|
|
138
|
+
owner_domain, target_domain, description, assignee, deadline,
|
|
139
|
+
priority, status, source, meeting_id, created_at, updated_at
|
|
140
|
+
)
|
|
141
|
+
SELECT
|
|
142
|
+
NULLIF(btrim(item->>'owner'), ''),
|
|
143
|
+
NULLIF(btrim(item->>'target'), ''),
|
|
144
|
+
btrim(item->>'description'),
|
|
145
|
+
NULLIF(btrim(item->>'assignee'), ''),
|
|
146
|
+
NULLIF(btrim(item->>'deadline'), '')::date,
|
|
147
|
+
COALESCE(NULLIF(btrim(item->>'priority'), ''), 'normal'),
|
|
148
|
+
'open',
|
|
149
|
+
'meeting',
|
|
150
|
+
v_meeting_id,
|
|
151
|
+
now(), now()
|
|
152
|
+
FROM jsonb_array_elements(p_action_items) AS item
|
|
153
|
+
WHERE COALESCE(btrim(item->>'description'), '') <> '';
|
|
154
|
+
END IF;
|
|
155
|
+
|
|
156
|
+
RETURN QUERY
|
|
157
|
+
SELECT v_meeting_id,
|
|
158
|
+
v_kb_id,
|
|
159
|
+
COALESCE(ARRAY(SELECT a.action_item_id
|
|
160
|
+
FROM semicolony.action_items a
|
|
161
|
+
WHERE a.meeting_id = v_meeting_id
|
|
162
|
+
ORDER BY a.created_at), '{}'::uuid[]),
|
|
163
|
+
(v_existing IS NOT NULL);
|
|
164
|
+
END;
|
|
165
|
+
$function$;
|
|
166
|
+
|
|
167
|
+
REVOKE ALL ON FUNCTION semicolony.record_meeting(
|
|
168
|
+
text, date, text, text, text, text, jsonb, text, text, jsonb, text, text, text, text, integer
|
|
169
|
+
) FROM PUBLIC;
|
|
170
|
+
|
|
171
|
+
GRANT EXECUTE ON FUNCTION semicolony.record_meeting(
|
|
172
|
+
text, date, text, text, text, text, jsonb, text, text, jsonb, text, text, text, text, integer
|
|
173
|
+
) TO sc_app;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
-- 202_team_members_record_of_authority.sql
|
|
2
|
+
--
|
|
3
|
+
-- Before this migration the only test for "is this person a team member" was
|
|
4
|
+
-- the string 'team' inside ontology.tags (14 of 41 person rows, measured
|
|
5
|
+
-- 2026-08-12). People, agents, and worker devices lived in three tables with
|
|
6
|
+
-- no foreign key between them: ontology (person 41 / agents 13), bot_status
|
|
7
|
+
-- (41), and worker_devices.worker_id. reus/roki/jay matched an ontology domain
|
|
8
|
+
-- by slug coincidence, not by constraint.
|
|
9
|
+
--
|
|
10
|
+
-- This migration is additive. No existing writer changes behavior. The
|
|
11
|
+
-- action_items cutover is migration 199.
|
|
12
|
+
|
|
13
|
+
CREATE TABLE IF NOT EXISTS semicolony.team_members (
|
|
14
|
+
member_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
15
|
+
domain varchar(100) NOT NULL UNIQUE REFERENCES semicolony.ontology(domain),
|
|
16
|
+
display_name text NOT NULL,
|
|
17
|
+
handle text,
|
|
18
|
+
role text,
|
|
19
|
+
status text NOT NULL DEFAULT 'active'
|
|
20
|
+
CHECK (status IN ('active', 'inactive', 'left')),
|
|
21
|
+
joined_at date,
|
|
22
|
+
left_at date,
|
|
23
|
+
slack_user_id text,
|
|
24
|
+
gitlab_username text,
|
|
25
|
+
github_login text,
|
|
26
|
+
auth_user_id uuid,
|
|
27
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
28
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
29
|
+
CONSTRAINT team_members_left_requires_date
|
|
30
|
+
CHECK (status <> 'left' OR left_at IS NOT NULL)
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
COMMENT ON TABLE semicolony.team_members IS
|
|
34
|
+
'Record of authority for Semicolon team people. Referenced externally by domain.';
|
|
35
|
+
|
|
36
|
+
-- Seed from the tag convention this table replaces, UNION the owner of every
|
|
37
|
+
-- registered human worker device. The tag alone is not sufficient: measured
|
|
38
|
+
-- 2026-08-12, jay owns jay-pc but carries tags={person}, so a tag-only seed
|
|
39
|
+
-- would leave jay-pc.member_id NULL and every feedback capture from that
|
|
40
|
+
-- device would fail resolving its origin member.
|
|
41
|
+
-- display_name falls back to o.domain when description is NULL, empty, or
|
|
42
|
+
-- has no ' — ' separator. Measured 2026-08-12: 15 eligible rows, 0 NULL/empty,
|
|
43
|
+
-- 0 missing the separator, so split_part(...,1) alone works today -- but
|
|
44
|
+
-- ontology.description is nullable and a snapshot is not a guarantee. A
|
|
45
|
+
-- coordinated maintenance window (this migration runs inside one) is
|
|
46
|
+
-- exactly where an unguarded NULL turning into a NOT NULL-violating INSERT
|
|
47
|
+
-- would abort loudest.
|
|
48
|
+
INSERT INTO semicolony.team_members (domain, display_name, role)
|
|
49
|
+
SELECT o.domain,
|
|
50
|
+
coalesce(nullif(btrim(split_part(o.description, ' — ', 1)), ''), o.domain),
|
|
51
|
+
nullif(split_part(o.description, ' — ', 2), '')
|
|
52
|
+
FROM semicolony.ontology o
|
|
53
|
+
WHERE o.entity_type = 'person'
|
|
54
|
+
AND (
|
|
55
|
+
'team' = ANY(tags)
|
|
56
|
+
OR o.domain IN (
|
|
57
|
+
SELECT wd.worker_id
|
|
58
|
+
FROM semicolony.worker_devices wd
|
|
59
|
+
WHERE wd.installation_kind = 'human-device'
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
ON CONFLICT (domain) DO NOTHING;
|
|
63
|
+
|
|
64
|
+
-- Fail loudly rather than silently leaving a worker unbound.
|
|
65
|
+
DO $$
|
|
66
|
+
DECLARE
|
|
67
|
+
v_missing text;
|
|
68
|
+
BEGIN
|
|
69
|
+
SELECT string_agg(DISTINCT wd.worker_id, ', ')
|
|
70
|
+
INTO v_missing
|
|
71
|
+
FROM semicolony.worker_devices wd
|
|
72
|
+
WHERE wd.installation_kind = 'human-device'
|
|
73
|
+
AND NOT EXISTS (
|
|
74
|
+
SELECT 1 FROM semicolony.team_members tm WHERE tm.domain = wd.worker_id
|
|
75
|
+
);
|
|
76
|
+
IF v_missing IS NOT NULL THEN
|
|
77
|
+
RAISE EXCEPTION
|
|
78
|
+
'human worker owners have no ontology person row: %. Add them to ontology before applying.',
|
|
79
|
+
v_missing USING ERRCODE = '22023';
|
|
80
|
+
END IF;
|
|
81
|
+
END
|
|
82
|
+
$$;
|
|
83
|
+
|
|
84
|
+
ALTER TABLE semicolony.worker_devices
|
|
85
|
+
ADD COLUMN IF NOT EXISTS member_id uuid REFERENCES semicolony.team_members(member_id);
|
|
86
|
+
|
|
87
|
+
-- Bind each human device to its person. worker_id is the human identity;
|
|
88
|
+
-- device_id is the machine. agent-runtime installations keep member_id NULL.
|
|
89
|
+
UPDATE semicolony.worker_devices wd
|
|
90
|
+
SET member_id = tm.member_id
|
|
91
|
+
FROM semicolony.team_members tm
|
|
92
|
+
WHERE tm.domain = wd.worker_id
|
|
93
|
+
AND wd.installation_kind = 'human-device'
|
|
94
|
+
AND wd.member_id IS DISTINCT FROM tm.member_id;
|
|
95
|
+
|
|
96
|
+
ALTER TABLE semicolony.worker_devices
|
|
97
|
+
ADD CONSTRAINT worker_devices_device_member_uk
|
|
98
|
+
UNIQUE (device_id, member_id);
|
|
99
|
+
|
|
100
|
+
-- Required target key for the action_items project FK in migration 199.
|
|
101
|
+
ALTER TABLE semicolony.ontology
|
|
102
|
+
ADD CONSTRAINT ontology_domain_entity_uk
|
|
103
|
+
UNIQUE (domain, entity_type);
|