@team-semicolon/semicolony-cli 4.18.105 → 4.18.107

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,61 @@
1
+ -- 201_meeting_audio_data_drop.sql
2
+ --
3
+ -- Drop `semo_ops.meetings.audio_data` now that the two recordings it held have
4
+ -- been preserved outside the database.
5
+ --
6
+ -- 198 deliberately did not drop this column. The design called for it —
7
+ -- recordings are restricted material and the worker deletes them once the
8
+ -- transcript exists, so a column storing them permanently contradicts the policy
9
+ -- it serves — but the pre-flight audit found 2 of 7 rows still holding 14 MB,
10
+ -- and neither meeting has a Discussion, so those were the only copies in
11
+ -- existence. 198 closed the policy hole with a NOT VALID CHECK instead and left
12
+ -- disposal as an explicit decision.
13
+ --
14
+ -- That decision has now been made and executed. Both recordings were exported,
15
+ -- their digests computed from the database bytes, transferred to the service
16
+ -- hive, and verified byte-identical after transfer:
17
+ --
18
+ -- 스페이스씨엘 회의 2026-03-19 b7d51487-… 7,839,157 B
19
+ -- sha256 01520aaf2fe0f4699fa3d164bbbe87794fa45c63e891698c85230ed80374c2c2
20
+ -- 스타스팟 회의 2026-03-28 a593c934-… 6,777,382 B
21
+ -- sha256 a6c991378a965983b6f4319b11422afbe62014881114a911a342a356b4044ab6
22
+ --
23
+ -- semicolony-agent:~/meeting-salvage/db-audio-rescue-20260813/ (README.md
24
+ -- records provenance and the intended final home)
25
+ --
26
+ -- That location is a holding area, not the designed home. Meeting audio belongs
27
+ -- in the team asset system as a `restricted` asset
28
+ -- (semo-ops docs/superpowers/specs/2026-08-06-semo-ops-assets-meeting-ingest-design.md),
29
+ -- which is blocked until the gateway implements the restricted tier. Dropping
30
+ -- the column does not depend on that work and should not wait for it: the bytes
31
+ -- are safe, and leaving a permanent audio column in the schema keeps the
32
+ -- contradiction alive.
33
+ --
34
+ -- Dropping the column also drops `meetings_audio_data_not_stored`, the CHECK
35
+ -- that 198 added to guard it. That is correct — a column that does not exist
36
+ -- cannot store audio.
37
+ --
38
+ -- Safety: refuse to run unless the rescue actually happened, expressed as the
39
+ -- exact two rows and byte counts that were exported. If the table has changed
40
+ -- since, this stops rather than destroying something unaccounted for.
41
+ --
42
+ -- The runner owns the transaction boundary, so no BEGIN/COMMIT here, and it
43
+ -- retargets the `semicolony.` qualifier to the active schema.
44
+
45
+ DO $$
46
+ DECLARE
47
+ v_rows int;
48
+ v_bytes bigint;
49
+ BEGIN
50
+ SELECT count(audio_data), coalesce(sum(length(audio_data)), 0)
51
+ INTO v_rows, v_bytes
52
+ FROM semicolony.meetings;
53
+
54
+ IF v_rows <> 2 OR v_bytes <> 14616539 THEN
55
+ RAISE EXCEPTION
56
+ 'refusing to drop audio_data: expected the 2 rescued rows totalling 14616539 bytes, found % row(s) totalling % bytes',
57
+ v_rows, v_bytes;
58
+ END IF;
59
+ END $$;
60
+
61
+ ALTER TABLE semicolony.meetings DROP COLUMN IF EXISTS audio_data;
@@ -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);