@team-semicolon/semicolony-cli 4.18.105 → 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 +1190 -1182
- 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,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);
|