sphica 0.6.28 → 0.6.30
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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +11 -5
- package/db/migrations/0010.check.sql +11 -0
- package/db/migrations/0010.sql +931 -0
- package/db/schema.sql +195 -54
- package/dist/capture.js +47 -4
- package/dist/cli.js +277 -4
- package/dist/deliver.js +187 -49
- package/dist/mcp-record.js +859 -308
- package/dist/mcp.js +277 -183
- package/hooks/hooks.json +13 -0
- package/package.json +1 -1
- package/skills/export/SKILL.md +3 -2
- package/skills/glean/SKILL.md +1 -1
- package/skills/review/reviewers/precedent.md +6 -1
- package/skills/rules/SKILL.md +5 -2
- package/skills/trace/SKILL.md +43 -7
|
@@ -0,0 +1,931 @@
|
|
|
1
|
+
-- Revision 9 → 10 of generation 2: AI adoption. Evidence gains the role decides and adoption the route agent; each record tool call is
|
|
2
|
+
-- logged (record_call), Claude Code's PreToolUse hook logs the turn of each (tool_call_observation), and a run names the call that began it.
|
|
3
|
+
-- Triggers and views are dropped first and all created again: rebuilding a table fails while a trigger names it.
|
|
4
|
+
-- Every statement matches db/schema.sql at revision 10; server/test/migrate.test.ts compares a migrated database with a fresh one.
|
|
5
|
+
|
|
6
|
+
drop trigger project_key_normal_insert;
|
|
7
|
+
drop trigger project_key_normal_update;
|
|
8
|
+
drop trigger source_owner_bound;
|
|
9
|
+
drop trigger session_cited;
|
|
10
|
+
drop trigger source_session_project;
|
|
11
|
+
drop trigger source_no_update;
|
|
12
|
+
drop trigger source_fts_ai;
|
|
13
|
+
drop trigger source_fts_ad;
|
|
14
|
+
drop trigger extraction_run_frozen;
|
|
15
|
+
drop trigger unit_insert_candidate;
|
|
16
|
+
drop trigger unit_run_project;
|
|
17
|
+
drop trigger unit_text_frozen;
|
|
18
|
+
drop trigger unit_revision_step;
|
|
19
|
+
drop trigger unit_lifecycle_via_state;
|
|
20
|
+
drop trigger unit_option_sealed;
|
|
21
|
+
drop trigger unit_option_frozen;
|
|
22
|
+
drop trigger unit_option_no_delete;
|
|
23
|
+
drop trigger unit_adoption_route;
|
|
24
|
+
drop trigger unit_link_frozen;
|
|
25
|
+
drop trigger unit_link_no_delete;
|
|
26
|
+
drop trigger unit_link_supersedes_acyclic;
|
|
27
|
+
drop trigger unit_state_rules;
|
|
28
|
+
drop trigger unit_state_append_only;
|
|
29
|
+
drop trigger unit_state_no_delete;
|
|
30
|
+
drop trigger unit_state_apply;
|
|
31
|
+
drop trigger unit_state_restore;
|
|
32
|
+
drop trigger unit_anchor_frozen;
|
|
33
|
+
drop trigger unit_anchor_no_delete;
|
|
34
|
+
drop trigger unit_alias_terms;
|
|
35
|
+
drop trigger unit_alias_frozen;
|
|
36
|
+
drop trigger unit_alias_no_delete;
|
|
37
|
+
drop trigger field_def_check;
|
|
38
|
+
drop trigger field_def_frozen;
|
|
39
|
+
drop trigger field_def_no_delete;
|
|
40
|
+
drop trigger unit_field_check;
|
|
41
|
+
drop trigger unit_field_frozen;
|
|
42
|
+
drop trigger unit_field_no_delete;
|
|
43
|
+
drop trigger unit_evidence_check;
|
|
44
|
+
drop trigger unit_evidence_retract;
|
|
45
|
+
drop trigger unit_evidence_no_delete;
|
|
46
|
+
drop trigger unit_adoption_no_delete;
|
|
47
|
+
drop trigger unit_evidence_retract_support;
|
|
48
|
+
drop trigger unit_adoption_retract_support;
|
|
49
|
+
drop trigger unit_anchor_retire_support;
|
|
50
|
+
drop trigger unit_adoption_check;
|
|
51
|
+
drop trigger unit_adoption_retract;
|
|
52
|
+
drop trigger unit_link_check;
|
|
53
|
+
drop trigger unit_state_project;
|
|
54
|
+
drop trigger unit_anchor_project;
|
|
55
|
+
drop trigger unit_alias_project;
|
|
56
|
+
drop trigger source_processing_project;
|
|
57
|
+
drop trigger unit_rev_evidence_i;
|
|
58
|
+
drop trigger unit_rev_evidence_u;
|
|
59
|
+
drop trigger unit_rev_adoption_i;
|
|
60
|
+
drop trigger unit_rev_adoption_u;
|
|
61
|
+
drop trigger unit_rev_evidence_d;
|
|
62
|
+
drop trigger unit_rev_adoption_d;
|
|
63
|
+
drop trigger unit_rev_link_i;
|
|
64
|
+
drop trigger unit_rev_link_u;
|
|
65
|
+
drop trigger unit_rev_anchor_i;
|
|
66
|
+
drop trigger unit_rev_anchor_u;
|
|
67
|
+
drop trigger unit_rev_alias_i;
|
|
68
|
+
drop trigger unit_rev_field_i;
|
|
69
|
+
drop trigger unit_rev_field_d;
|
|
70
|
+
drop trigger unit_fts_ai;
|
|
71
|
+
drop trigger unit_fts_ad;
|
|
72
|
+
drop trigger unit_fts_option_i;
|
|
73
|
+
drop trigger unit_fts_anchor_i;
|
|
74
|
+
drop trigger unit_fts_anchor_u;
|
|
75
|
+
drop trigger unit_fts_anchor_d;
|
|
76
|
+
drop trigger unit_fts_alias_i;
|
|
77
|
+
drop trigger unit_fts_alias_d;
|
|
78
|
+
drop trigger unit_fts_field_i;
|
|
79
|
+
drop trigger unit_fts_field_d;
|
|
80
|
+
drop trigger delivery_ad;
|
|
81
|
+
drop trigger ingest_source_insert;
|
|
82
|
+
drop trigger capture_session_insert;
|
|
83
|
+
drop trigger capture_message_insert;
|
|
84
|
+
drop trigger capture_edit_insert;
|
|
85
|
+
drop trigger capture_delivery_insert;
|
|
86
|
+
drop trigger capture_delivery_scoped_insert;
|
|
87
|
+
drop trigger capture_delivery_prune_insert;
|
|
88
|
+
drop view unit_support;
|
|
89
|
+
drop view unit_search_text;
|
|
90
|
+
drop view ingest_source;
|
|
91
|
+
drop view capture_session;
|
|
92
|
+
drop view capture_message;
|
|
93
|
+
drop view capture_edit;
|
|
94
|
+
drop view capture_delivery;
|
|
95
|
+
drop view capture_delivery_scoped;
|
|
96
|
+
drop view capture_delivery_prune;
|
|
97
|
+
|
|
98
|
+
create temp table sphica_migration_seq as select name, seq from sqlite_sequence where name in ('extraction_run', 'unit_evidence', 'unit_adoption');
|
|
99
|
+
|
|
100
|
+
create table record_call (
|
|
101
|
+
id integer primary key autoincrement not null,
|
|
102
|
+
project_id integer not null references project (id) on delete cascade,
|
|
103
|
+
tool text not null check (tool <> ''),
|
|
104
|
+
host text check (host in ('claude-code', 'codex')),
|
|
105
|
+
caller_session text,
|
|
106
|
+
caller_turn text,
|
|
107
|
+
tool_use_id text,
|
|
108
|
+
mode text not null check (mode in ('interactive', 'headless', 'sdk', 'unknown')),
|
|
109
|
+
mode_raw text,
|
|
110
|
+
called_at text not null check (strftime('%Y-%m-%dT%H:%M:%fZ', called_at) is called_at)
|
|
111
|
+
) strict;
|
|
112
|
+
create table tool_call_observation (
|
|
113
|
+
id integer primary key autoincrement not null,
|
|
114
|
+
host text not null check (host in ('claude-code', 'codex')),
|
|
115
|
+
session_external text not null check (session_external <> ''),
|
|
116
|
+
turn_id text,
|
|
117
|
+
tool_use_id text not null check (tool_use_id <> ''),
|
|
118
|
+
tool_name text not null check (tool_name <> ''),
|
|
119
|
+
owner_turn integer not null check (owner_turn in (0, 1)),
|
|
120
|
+
observed_at text not null check (strftime('%Y-%m-%dT%H:%M:%fZ', observed_at) is observed_at),
|
|
121
|
+
unique (host, tool_use_id)
|
|
122
|
+
) strict;
|
|
123
|
+
create table unit_replacement (
|
|
124
|
+
id integer primary key autoincrement not null,
|
|
125
|
+
from_unit integer not null references unit (id) on delete cascade,
|
|
126
|
+
to_unit integer not null references unit (id) on delete cascade,
|
|
127
|
+
run_id integer references extraction_run (id),
|
|
128
|
+
forget_id integer references forget_batch (id),
|
|
129
|
+
started_at text not null check (strftime('%Y-%m-%dT%H:%M:%fZ', started_at) is started_at),
|
|
130
|
+
ended_at text check (strftime('%Y-%m-%dT%H:%M:%fZ', ended_at) is ended_at),
|
|
131
|
+
end_reason text,
|
|
132
|
+
end_run_id integer references extraction_run (id),
|
|
133
|
+
end_forget_id integer references forget_batch (id),
|
|
134
|
+
check ((run_id is null) <> (forget_id is null)),
|
|
135
|
+
check ((ended_at is null) = (end_reason is null)),
|
|
136
|
+
check (ended_at is null or (end_run_id is null) <> (end_forget_id is null)),
|
|
137
|
+
check (ended_at is not null or (end_run_id is null and end_forget_id is null)),
|
|
138
|
+
check (ended_at >= started_at),
|
|
139
|
+
check (from_unit <> to_unit)
|
|
140
|
+
) strict;
|
|
141
|
+
create table unit_replacement_gap (
|
|
142
|
+
from_unit integer not null references unit (id) on delete cascade,
|
|
143
|
+
to_unit integer not null references unit (id) on delete cascade,
|
|
144
|
+
run_id integer not null references extraction_run (id),
|
|
145
|
+
primary key (from_unit, to_unit)
|
|
146
|
+
) strict;
|
|
147
|
+
|
|
148
|
+
create table extraction_run_new (
|
|
149
|
+
id integer primary key autoincrement not null,
|
|
150
|
+
project_id integer not null references project (id) on delete cascade,
|
|
151
|
+
origin text not null check (origin in ('trace', 'harvest', 'glean', 'migration')),
|
|
152
|
+
target text not null,
|
|
153
|
+
session_id text references session (id) on delete set null,
|
|
154
|
+
status text not null check (status in ('running', 'saved')),
|
|
155
|
+
input_bytes integer check (input_bytes >= 0),
|
|
156
|
+
-- The CLI-issued draft this run saves. A saved run's draft saves nothing again; the draft is bound to this run's project and target
|
|
157
|
+
draft_id text unique,
|
|
158
|
+
-- The record tool call that began this run; save compares its own caller with it
|
|
159
|
+
begin_call_id integer references record_call (id),
|
|
160
|
+
started_at text not null check (strftime('%Y-%m-%dT%H:%M:%fZ', started_at) is started_at),
|
|
161
|
+
finished_at text check (strftime('%Y-%m-%dT%H:%M:%fZ', finished_at) is finished_at),
|
|
162
|
+
check (finished_at >= started_at)
|
|
163
|
+
) strict;
|
|
164
|
+
insert into extraction_run_new (id, project_id, origin, target, session_id, status, input_bytes, draft_id, started_at, finished_at) select id, project_id, origin, target, session_id, status, input_bytes, draft_id, started_at, finished_at from extraction_run;
|
|
165
|
+
drop table extraction_run;
|
|
166
|
+
alter table extraction_run_new rename to extraction_run;
|
|
167
|
+
|
|
168
|
+
create table unit_evidence_new (
|
|
169
|
+
id integer primary key autoincrement not null,
|
|
170
|
+
unit_id integer not null references unit (id) on delete cascade,
|
|
171
|
+
option_id integer,
|
|
172
|
+
source_id integer not null references source (id) on delete cascade,
|
|
173
|
+
span_start integer not null check (span_start >= 0),
|
|
174
|
+
span_end integer not null check (span_end > span_start),
|
|
175
|
+
role text not null check (role in ('states', 'proposes', 'rejects', 'explains', 'implements', 'reconsiders', 'decides')),
|
|
176
|
+
-- A third party the owner reported ("X said ..."): hearsay by the owner, never X's own statement
|
|
177
|
+
reported_speaker text,
|
|
178
|
+
run_id integer not null references extraction_run (id),
|
|
179
|
+
added_at text not null check (strftime('%Y-%m-%dT%H:%M:%fZ', added_at) is added_at),
|
|
180
|
+
retracted_at text check (strftime('%Y-%m-%dT%H:%M:%fZ', retracted_at) is retracted_at),
|
|
181
|
+
retraction_reason text,
|
|
182
|
+
retraction_source_id integer references source (id) on delete cascade,
|
|
183
|
+
retraction_span_start integer,
|
|
184
|
+
retraction_span_end integer,
|
|
185
|
+
foreign key (unit_id, option_id) references unit_option (unit_id, id) on delete cascade,
|
|
186
|
+
check ((retracted_at is null) = (retraction_reason is null)),
|
|
187
|
+
check ((retracted_at is null) = (retraction_source_id is null)),
|
|
188
|
+
check ((retraction_source_id is null) = (retraction_span_start is null)),
|
|
189
|
+
check ((retraction_source_id is null) = (retraction_span_end is null)),
|
|
190
|
+
check (retraction_span_end is null or retraction_span_end > retraction_span_start),
|
|
191
|
+
check (retraction_span_start >= 0),
|
|
192
|
+
check (retracted_at >= added_at)
|
|
193
|
+
) strict;
|
|
194
|
+
insert into unit_evidence_new (id, unit_id, option_id, source_id, span_start, span_end, role, reported_speaker, run_id, added_at, retracted_at, retraction_reason, retraction_source_id, retraction_span_start, retraction_span_end) select id, unit_id, option_id, source_id, span_start, span_end, role, reported_speaker, run_id, added_at, retracted_at, retraction_reason, retraction_source_id, retraction_span_start, retraction_span_end from unit_evidence;
|
|
195
|
+
drop table unit_evidence;
|
|
196
|
+
alter table unit_evidence_new rename to unit_evidence;
|
|
197
|
+
|
|
198
|
+
create table unit_adoption_new (
|
|
199
|
+
id integer primary key autoincrement not null,
|
|
200
|
+
unit_id integer not null references unit (id) on delete cascade,
|
|
201
|
+
route text not null check (route in ('owner_statement', 'explicit', 'agent')),
|
|
202
|
+
source_id integer not null references source (id) on delete cascade,
|
|
203
|
+
span_start integer not null check (span_start >= 0),
|
|
204
|
+
span_end integer not null check (span_end > span_start),
|
|
205
|
+
run_id integer not null references extraction_run (id),
|
|
206
|
+
added_at text not null check (strftime('%Y-%m-%dT%H:%M:%fZ', added_at) is added_at),
|
|
207
|
+
retracted_at text check (strftime('%Y-%m-%dT%H:%M:%fZ', retracted_at) is retracted_at),
|
|
208
|
+
retraction_reason text,
|
|
209
|
+
retraction_source_id integer references source (id) on delete cascade,
|
|
210
|
+
retraction_span_start integer,
|
|
211
|
+
retraction_span_end integer,
|
|
212
|
+
unique (unit_id, source_id, span_start, span_end),
|
|
213
|
+
check ((retracted_at is null) = (retraction_reason is null)),
|
|
214
|
+
check ((retracted_at is null) = (retraction_source_id is null)),
|
|
215
|
+
check ((retraction_source_id is null) = (retraction_span_start is null)),
|
|
216
|
+
check ((retraction_source_id is null) = (retraction_span_end is null)),
|
|
217
|
+
check (retraction_span_end is null or retraction_span_end > retraction_span_start),
|
|
218
|
+
check (retraction_span_start >= 0),
|
|
219
|
+
check (retracted_at >= added_at)
|
|
220
|
+
) strict;
|
|
221
|
+
insert into unit_adoption_new (id, unit_id, route, source_id, span_start, span_end, run_id, added_at, retracted_at, retraction_reason, retraction_source_id, retraction_span_start, retraction_span_end) select id, unit_id, route, source_id, span_start, span_end, run_id, added_at, retracted_at, retraction_reason, retraction_source_id, retraction_span_start, retraction_span_end from unit_adoption;
|
|
222
|
+
drop table unit_adoption;
|
|
223
|
+
alter table unit_adoption_new rename to unit_adoption;
|
|
224
|
+
|
|
225
|
+
-- A rebuilt table's counter restarts from its largest copied id; keep the larger of that and the old counter
|
|
226
|
+
update sqlite_sequence set seq = max(seq, (select seq from temp.sphica_migration_seq m where m.name = sqlite_sequence.name))
|
|
227
|
+
where name in (select name from temp.sphica_migration_seq);
|
|
228
|
+
insert into sqlite_sequence (name, seq) select name, seq from temp.sphica_migration_seq m where not exists (select 1 from sqlite_sequence s where s.name = m.name);
|
|
229
|
+
drop table temp.sphica_migration_seq;
|
|
230
|
+
|
|
231
|
+
-- Replacements from what revision-9 history proves. Every project with records gets a migration run for the rows and states written
|
|
232
|
+
-- here and when the step judges every record afterwards (server/src/reconcile.ts settleForMigration).
|
|
233
|
+
create temp table sphica_migration_note (rule text, item text, action text);
|
|
234
|
+
insert into extraction_run (project_id, origin, target, status, started_at, finished_at)
|
|
235
|
+
select distinct project_id, 'migration', 'revision:10', 'saved', strftime('%Y-%m-%dT%H:%M:%fZ', 'now'), strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
|
236
|
+
from unit;
|
|
237
|
+
create temp table sphica_migration_run as
|
|
238
|
+
select project_id, max(id) as id from extraction_run where origin = 'migration' and target = 'revision:10' group by project_id;
|
|
239
|
+
|
|
240
|
+
-- In effect now: a superseded record and the live successor revision 9 let hold its one place, from when it was last superseded
|
|
241
|
+
insert into unit_replacement (from_unit, to_unit, run_id, started_at)
|
|
242
|
+
select l.from_unit, l.to_unit, r.id,
|
|
243
|
+
coalesce((select max(t.at) from unit_state t where t.unit_id = o.id and t.to_state = 'superseded'), strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
|
244
|
+
from unit_link l join unit o on o.id = l.to_unit join unit s on s.id = l.from_unit join temp.sphica_migration_run r on r.project_id = o.project_id
|
|
245
|
+
where l.kind = 'supersedes' and o.lifecycle = 'superseded' and s.lifecycle in ('active', 'superseded')
|
|
246
|
+
and s.extraction = 'supported' and s.unsourced = 0
|
|
247
|
+
and l.from_unit = (select min(k.from_unit) from unit_link k join unit ks on ks.id = k.from_unit where k.to_unit = o.id
|
|
248
|
+
and k.kind = 'supersedes' and ks.lifecycle in ('active', 'superseded') and ks.extraction = 'supported' and ks.unsourced = 0);
|
|
249
|
+
insert into sphica_migration_note
|
|
250
|
+
select 'a replacement in effect, dated from when the record was last superseded', s.key || ' → ' || o.key, 'in effect since ' || x.started_at
|
|
251
|
+
from unit_replacement x join unit s on s.id = x.from_unit join unit o on o.id = x.to_unit where x.ended_at is null;
|
|
252
|
+
|
|
253
|
+
-- Ended in the past: the successor in effect (active until then) was withdrawn, which brought the record back at that very moment
|
|
254
|
+
-- (revision 9 wrote both together, the withdrawal first). Each restoration pairs with the last such withdrawal before it, and starts at
|
|
255
|
+
-- the record's last superseded state before it, by state order: times alone repeat within one millisecond. A proposal withdrawn in the
|
|
256
|
+
-- same save was never in effect, so it proves nothing
|
|
257
|
+
insert into unit_replacement (from_unit, to_unit, run_id, started_at, ended_at, end_reason, end_run_id)
|
|
258
|
+
select l.from_unit, l.to_unit, r.id, b.started, w.at, s.key || ' was withdrawn', r.id
|
|
259
|
+
from (select t.id, t.unit_id, t.at,
|
|
260
|
+
(select p.at from unit_state p where p.unit_id = t.unit_id and p.to_state = 'superseded' and p.id < t.id order by p.id desc limit 1) as started,
|
|
261
|
+
(select max(w2.id) from unit_state w2 join unit_link l2 on l2.from_unit = w2.unit_id and l2.to_unit = t.unit_id and l2.kind = 'supersedes'
|
|
262
|
+
where w2.to_state = 'withdrawn' and w2.from_state = 'active' and w2.at = t.at and w2.id < t.id) as cause
|
|
263
|
+
from unit_state t where t.from_state = 'superseded' and t.to_state = 'candidate' and t.reason = 'its successor was withdrawn') b
|
|
264
|
+
join unit_state w on w.id = b.cause
|
|
265
|
+
join unit_link l on l.from_unit = w.unit_id and l.to_unit = b.unit_id and l.kind = 'supersedes'
|
|
266
|
+
join unit s on s.id = l.from_unit join unit o on o.id = l.to_unit
|
|
267
|
+
join temp.sphica_migration_run r on r.project_id = o.project_id
|
|
268
|
+
where b.started is not null;
|
|
269
|
+
insert into sphica_migration_note
|
|
270
|
+
select 'a past replacement, proven by the withdrawal that ended it', s.key || ' → ' || o.key, 'from ' || x.started_at || ' to ' || x.ended_at
|
|
271
|
+
from unit_replacement x join unit s on s.id = x.from_unit join unit o on o.id = x.to_unit where x.ended_at is not null;
|
|
272
|
+
|
|
273
|
+
-- An intent whose successor was once active but whose effect no history dates: marked, so read never calls it a mere proposal
|
|
274
|
+
insert into unit_replacement_gap (from_unit, to_unit, run_id)
|
|
275
|
+
select l.from_unit, l.to_unit, r.id
|
|
276
|
+
from unit_link l join unit o on o.id = l.to_unit join temp.sphica_migration_run r on r.project_id = o.project_id
|
|
277
|
+
where l.kind = 'supersedes'
|
|
278
|
+
and not exists (select 1 from unit_replacement x where x.from_unit = l.from_unit and x.to_unit = l.to_unit)
|
|
279
|
+
and exists (select 1 from unit_state t where t.unit_id = l.from_unit and t.to_state = 'active');
|
|
280
|
+
insert into sphica_migration_note
|
|
281
|
+
select 'an intent whose earlier effect is not recorded', s.key || ' → ' || o.key, 'marked: read says its history was not recorded'
|
|
282
|
+
from unit_replacement_gap g join unit s on s.id = g.from_unit join unit o on o.id = g.to_unit;
|
|
283
|
+
-- Reads now show history these records did not have: a change made against their earlier revision is stale
|
|
284
|
+
update unit set revision = revision + 1 where id in (
|
|
285
|
+
select from_unit from unit_replacement union select to_unit from unit_replacement
|
|
286
|
+
union select from_unit from unit_replacement_gap union select to_unit from unit_replacement_gap);
|
|
287
|
+
drop table temp.sphica_migration_run;
|
|
288
|
+
|
|
289
|
+
create index record_call_project on record_call (project_id, host, called_at);
|
|
290
|
+
create index record_call_caller on record_call (host, caller_session, caller_turn);
|
|
291
|
+
create index record_call_tool_use on record_call (tool_use_id) where tool_use_id is not null;
|
|
292
|
+
create index extraction_run_project on extraction_run (project_id);
|
|
293
|
+
create index extraction_run_session on extraction_run (session_id) where session_id is not null;
|
|
294
|
+
create index extraction_run_begin_call on extraction_run (begin_call_id) where begin_call_id is not null;
|
|
295
|
+
create unique index unit_evidence_unit_once on unit_evidence (unit_id, source_id, span_start, span_end, role) where option_id is null;
|
|
296
|
+
create unique index unit_evidence_option_once on unit_evidence (option_id, source_id, span_start, span_end, role) where option_id is not null;
|
|
297
|
+
create index unit_evidence_source on unit_evidence (source_id);
|
|
298
|
+
create index unit_evidence_option on unit_evidence (unit_id, option_id);
|
|
299
|
+
create index unit_evidence_retraction on unit_evidence (retraction_source_id) where retraction_source_id is not null;
|
|
300
|
+
create index unit_evidence_run on unit_evidence (run_id);
|
|
301
|
+
create index unit_adoption_source on unit_adoption (source_id);
|
|
302
|
+
create index unit_adoption_retraction on unit_adoption (retraction_source_id) where retraction_source_id is not null;
|
|
303
|
+
create index unit_adoption_run on unit_adoption (run_id);
|
|
304
|
+
create unique index unit_link_one_intent on unit_link (from_unit) where kind = 'supersedes';
|
|
305
|
+
create unique index unit_replacement_place on unit_replacement (to_unit) where ended_at is null;
|
|
306
|
+
create index unit_replacement_from on unit_replacement (from_unit);
|
|
307
|
+
create index unit_replacement_to on unit_replacement (to_unit);
|
|
308
|
+
create index unit_replacement_run on unit_replacement (run_id) where run_id is not null;
|
|
309
|
+
create index unit_replacement_forget on unit_replacement (forget_id) where forget_id is not null;
|
|
310
|
+
create index unit_replacement_end_run on unit_replacement (end_run_id) where end_run_id is not null;
|
|
311
|
+
create index unit_replacement_end_forget on unit_replacement (end_forget_id) where end_forget_id is not null;
|
|
312
|
+
create index unit_replacement_gap_to on unit_replacement_gap (to_unit);
|
|
313
|
+
create index unit_replacement_gap_run on unit_replacement_gap (run_id);
|
|
314
|
+
create trigger project_key_normal_insert before insert on project when new.key glob 'git:*' begin
|
|
315
|
+
select raise(abort, 'the project key is not normalized')
|
|
316
|
+
from (select rest, case when instr(rest, '/') = 0 then rest else substr(rest, 1, instr(rest, '/') - 1) end as host
|
|
317
|
+
from (select substr(new.key, 5) as rest))
|
|
318
|
+
where new.key is not case when lower(host) = 'github.com' then lower(new.key) else 'git:' || lower(host) || substr(rest, length(host) + 1) end;
|
|
319
|
+
end;
|
|
320
|
+
create trigger project_key_normal_update before update of key on project when new.key glob 'git:*' begin
|
|
321
|
+
select raise(abort, 'the project key is not normalized')
|
|
322
|
+
from (select rest, case when instr(rest, '/') = 0 then rest else substr(rest, 1, instr(rest, '/') - 1) end as host
|
|
323
|
+
from (select substr(new.key, 5) as rest))
|
|
324
|
+
where new.key is not case when lower(host) = 'github.com' then lower(new.key) else 'git:' || lower(host) || substr(rest, length(host) + 1) end;
|
|
325
|
+
end;
|
|
326
|
+
create trigger source_owner_bound before insert on source
|
|
327
|
+
when new.kind <> 'session_message' and new.author_kind = 'owner'
|
|
328
|
+
and not exists (select 1 from owner_identity where provider = 'github' and external_id = new.author_external_id) begin
|
|
329
|
+
select raise(abort, 'owner authorship needs a bound owner identity');
|
|
330
|
+
end;
|
|
331
|
+
create trigger session_cited before delete on session
|
|
332
|
+
when exists (select 1 from project where id = old.project_id) and (
|
|
333
|
+
exists (select 1 from source s where s.session_id = old.id and (
|
|
334
|
+
exists (select 1 from unit_evidence e where e.source_id = s.id or e.retraction_source_id = s.id)
|
|
335
|
+
or exists (select 1 from unit_adoption a where a.source_id = s.id or a.retraction_source_id = s.id)
|
|
336
|
+
or exists (select 1 from field_def d where d.source_id = s.id)
|
|
337
|
+
or exists (select 1 from unit_field f where f.source_id = s.id)
|
|
338
|
+
or exists (select 1 from unit_state t where t.source_id = s.id)))
|
|
339
|
+
or exists (select 1 from edit_observation o join unit_anchor a on a.edit_observation_id = o.id where o.session_id = old.id)) begin
|
|
340
|
+
select raise(abort, 'records cite this session; forget its messages with /sphica:forget rather than deleting it');
|
|
341
|
+
end;
|
|
342
|
+
create trigger source_session_project before insert on source when new.session_id is not null
|
|
343
|
+
and not exists (select 1 from session where id = new.session_id and project_id = new.project_id) begin
|
|
344
|
+
select raise(abort, 'source and session belong to different projects');
|
|
345
|
+
end;
|
|
346
|
+
create trigger source_no_update before update on source begin
|
|
347
|
+
select raise(abort, 'sources are never rewritten; capture a new revision');
|
|
348
|
+
end;
|
|
349
|
+
create trigger source_fts_ai after insert on source when new.indexed = 1 begin
|
|
350
|
+
insert into source_fts (rowid, lexemes) values (new.id, sphica_terms(new.text));
|
|
351
|
+
end;
|
|
352
|
+
create trigger source_fts_ad after delete on source when old.indexed = 1 begin
|
|
353
|
+
delete from source_fts where rowid = old.id;
|
|
354
|
+
end;
|
|
355
|
+
create trigger record_call_frozen before update on record_call begin
|
|
356
|
+
select raise(abort, 'record tool calls are never changed');
|
|
357
|
+
end;
|
|
358
|
+
create trigger extraction_run_call_project before insert on extraction_run when new.begin_call_id is not null begin
|
|
359
|
+
select raise(abort, 'a run begins from a record tool call of its own project')
|
|
360
|
+
where (select project_id from record_call where id = new.begin_call_id) is not new.project_id;
|
|
361
|
+
end;
|
|
362
|
+
create trigger extraction_run_frozen before update on extraction_run
|
|
363
|
+
when new.id is not old.id or new.project_id is not old.project_id or new.origin is not old.origin or new.target is not old.target
|
|
364
|
+
or new.input_bytes is not old.input_bytes or new.draft_id is not old.draft_id or new.begin_call_id is not old.begin_call_id
|
|
365
|
+
or new.started_at is not old.started_at
|
|
366
|
+
or (new.session_id is not old.session_id and (new.session_id is not null or exists (select 1 from session where id = old.session_id)))
|
|
367
|
+
or ((new.status is not old.status or new.finished_at is not old.finished_at) and old.status <> 'running') begin
|
|
368
|
+
select raise(abort, 'a run changes once, when it finishes');
|
|
369
|
+
end;
|
|
370
|
+
create trigger unit_insert_candidate before insert on unit when new.lifecycle <> 'candidate' begin
|
|
371
|
+
select raise(abort, 'units start as candidates');
|
|
372
|
+
end;
|
|
373
|
+
create trigger unit_run_project before insert on unit
|
|
374
|
+
when not exists (select 1 from extraction_run where id = new.run_id and project_id = new.project_id) begin
|
|
375
|
+
select raise(abort, 'unit and run belong to different projects');
|
|
376
|
+
end;
|
|
377
|
+
create trigger unit_text_frozen before update of project_id, key, kind, stance, text, why, scope_note, revisit_when, no_code_surface,
|
|
378
|
+
extraction, extraction_reason, unsourced, run_id, created_at, content_hash
|
|
379
|
+
on unit begin
|
|
380
|
+
select raise(abort, 'unit text is never rewritten; record a successor');
|
|
381
|
+
end;
|
|
382
|
+
create trigger unit_revision_step before update of revision on unit when new.revision is not old.revision + 1 begin
|
|
383
|
+
select raise(abort, 'a unit''s revision rises by one with each change to its relations');
|
|
384
|
+
end;
|
|
385
|
+
create trigger unit_lifecycle_via_state before update of lifecycle on unit
|
|
386
|
+
when new.lifecycle is not (select to_state from unit_state where unit_id = new.id order by id desc limit 1) begin
|
|
387
|
+
select raise(abort, 'lifecycle changes only through unit_state');
|
|
388
|
+
end;
|
|
389
|
+
create trigger unit_option_sealed before insert on unit_option
|
|
390
|
+
when exists (select 1 from unit_state where unit_id = new.unit_id) or exists (select 1 from unit_alias where unit_id = new.unit_id) begin
|
|
391
|
+
select raise(abort, 'options are written with the unit, before its first state; record a successor instead');
|
|
392
|
+
end;
|
|
393
|
+
create trigger unit_option_frozen before update on unit_option begin
|
|
394
|
+
select raise(abort, 'options are never rewritten; record a successor');
|
|
395
|
+
end;
|
|
396
|
+
create trigger unit_option_no_delete before delete on unit_option
|
|
397
|
+
when exists (select 1 from unit where id = old.unit_id) begin
|
|
398
|
+
select raise(abort, 'options are never removed on their own');
|
|
399
|
+
end;
|
|
400
|
+
create trigger unit_adoption_route before insert on unit_adoption begin
|
|
401
|
+
select raise(abort, 'owner_statement adoption needs an owner-authored source')
|
|
402
|
+
where new.route = 'owner_statement' and not exists (select 1 from source where id = new.source_id and author_kind = 'owner');
|
|
403
|
+
select raise(abort, 'explicit adoption needs the owner or a maintainer (OWNER, MEMBER, COLLABORATOR association)')
|
|
404
|
+
where new.route = 'explicit' and not exists (select 1 from source where id = new.source_id
|
|
405
|
+
and (author_kind = 'owner' or author_association in ('OWNER', 'MEMBER', 'COLLABORATOR')));
|
|
406
|
+
select raise(abort, 'a merge is not adoption')
|
|
407
|
+
where exists (select 1 from source where id = new.source_id and kind = 'pr_event');
|
|
408
|
+
select raise(abort, 'adoption applies to decisions and constraints')
|
|
409
|
+
where not exists (select 1 from unit where id = new.unit_id and kind in ('decision', 'constraint'));
|
|
410
|
+
select raise(abort, 'agent adoption pairs with live decides evidence on the same span of the unit')
|
|
411
|
+
where new.route = 'agent' and not exists (select 1 from unit_evidence e where e.unit_id = new.unit_id and e.option_id is null
|
|
412
|
+
and e.role = 'decides' and e.source_id = new.source_id and e.span_start = new.span_start and e.span_end = new.span_end
|
|
413
|
+
and e.retracted_at is null);
|
|
414
|
+
select raise(abort, 'agent adoption needs a trace run')
|
|
415
|
+
where new.route = 'agent' and not exists (select 1 from extraction_run where id = new.run_id and origin = 'trace');
|
|
416
|
+
select raise(abort, 'agent adoption needs a run begun by an interactive session')
|
|
417
|
+
where new.route = 'agent' and not exists (select 1 from extraction_run r join record_call c on c.id = r.begin_call_id
|
|
418
|
+
where r.id = new.run_id and c.mode = 'interactive');
|
|
419
|
+
select raise(abort, 'agent adoption cannot cite a reply from a turn that ran a record tool, or one no record tool call can be placed away from')
|
|
420
|
+
where new.route = 'agent' and exists (select 1 from agent_ineligible_source where source_id = new.source_id);
|
|
421
|
+
select raise(abort, 'agent adoption cites a reply of the session the trace reads')
|
|
422
|
+
where new.route = 'agent' and not exists (select 1 from source s join extraction_run r on r.id = new.run_id
|
|
423
|
+
where s.id = new.source_id and s.session_id = r.session_id);
|
|
424
|
+
end;
|
|
425
|
+
create trigger unit_link_frozen before update on unit_link begin
|
|
426
|
+
select raise(abort, 'links are frozen; only an unresolved conflict can be resolved, once')
|
|
427
|
+
where new.from_unit is not old.from_unit or new.to_unit is not old.to_unit or new.kind is not old.kind
|
|
428
|
+
or new.run_id is not old.run_id or new.added_at is not old.added_at or old.resolved_at is not null or old.kind <> 'conflicts';
|
|
429
|
+
end;
|
|
430
|
+
create trigger unit_link_no_delete before delete on unit_link
|
|
431
|
+
when exists (select 1 from unit where id = old.from_unit) and exists (select 1 from unit where id = old.to_unit) begin
|
|
432
|
+
select raise(abort, 'links are never removed on their own');
|
|
433
|
+
end;
|
|
434
|
+
create trigger unit_link_supersedes_acyclic before insert on unit_link when new.kind = 'supersedes' begin
|
|
435
|
+
select raise(abort, 'supersedes links cannot form a cycle')
|
|
436
|
+
where exists (
|
|
437
|
+
with recursive chain(id) as (
|
|
438
|
+
select new.to_unit union select l.to_unit from unit_link l join chain on l.from_unit = chain.id where l.kind = 'supersedes')
|
|
439
|
+
select 1 from chain where id = new.from_unit);
|
|
440
|
+
end;
|
|
441
|
+
create trigger unit_replacement_check before insert on unit_replacement begin
|
|
442
|
+
select raise(abort, 'a replacement''s cause belongs to the project of the records it joins')
|
|
443
|
+
where exists (select 1 from extraction_run where id = new.run_id
|
|
444
|
+
and project_id is not (select project_id from unit where id = new.to_unit))
|
|
445
|
+
or exists (select 1 from forget_batch where id = new.forget_id
|
|
446
|
+
and project_id is not (select project_id from unit where id = new.to_unit));
|
|
447
|
+
select raise(abort, 'a replacement takes effect only from the record''s own intent to replace that one')
|
|
448
|
+
where not exists (select 1 from unit_link where from_unit = new.from_unit and to_unit = new.to_unit and kind = 'supersedes');
|
|
449
|
+
select raise(abort, 'a replacement starts open')
|
|
450
|
+
where new.ended_at is not null;
|
|
451
|
+
-- A record whose source is gone can be replaced (that is how it is fixed); a quarantined one, never active, cannot
|
|
452
|
+
select raise(abort, 'a replacement takes effect only from a sound successor into a record not quarantined nor withdrawn')
|
|
453
|
+
where exists (select 1 from unit where id = new.from_unit and (extraction <> 'supported' or unsourced = 1 or lifecycle = 'withdrawn'))
|
|
454
|
+
or exists (select 1 from unit where id = new.to_unit and (extraction <> 'supported' or lifecycle = 'withdrawn'));
|
|
455
|
+
-- Of a decision or constraint, only the owner's or a maintainer's adoption lets a replacement take effect, whatever it replaces
|
|
456
|
+
select raise(abort, 'a decision or constraint replaces another only with the owner''s or a maintainer''s adoption')
|
|
457
|
+
where exists (select 1 from unit where id = new.from_unit and kind in ('decision', 'constraint'))
|
|
458
|
+
and not exists (select 1 from unit_adoption where unit_id = new.from_unit and route in ('owner_statement', 'explicit') and retracted_at is null);
|
|
459
|
+
select raise(abort, 'a replacement takes effect only from a sound successor with full support')
|
|
460
|
+
where exists (select 1 from unit_support where unit_id = new.from_unit and missing is not null);
|
|
461
|
+
end;
|
|
462
|
+
create trigger unit_replacement_end before update on unit_replacement begin
|
|
463
|
+
select raise(abort, 'a replacement''s cause belongs to the project of the records it joins')
|
|
464
|
+
where exists (select 1 from extraction_run where id = new.end_run_id
|
|
465
|
+
and project_id is not (select project_id from unit where id = new.to_unit))
|
|
466
|
+
or exists (select 1 from forget_batch where id = new.end_forget_id
|
|
467
|
+
and project_id is not (select project_id from unit where id = new.to_unit));
|
|
468
|
+
select raise(abort, 'a replacement only ever ends, once')
|
|
469
|
+
where old.ended_at is not null or new.ended_at is null or new.id is not old.id or new.from_unit is not old.from_unit
|
|
470
|
+
or new.to_unit is not old.to_unit or new.run_id is not old.run_id or new.forget_id is not old.forget_id
|
|
471
|
+
or new.started_at is not old.started_at;
|
|
472
|
+
end;
|
|
473
|
+
create trigger unit_replacement_no_delete before delete on unit_replacement
|
|
474
|
+
when exists (select 1 from unit where id = old.from_unit) and exists (select 1 from unit where id = old.to_unit) begin
|
|
475
|
+
select raise(abort, 'replacements are history and are never removed');
|
|
476
|
+
end;
|
|
477
|
+
create trigger unit_rev_replacement_i after insert on unit_replacement begin
|
|
478
|
+
update unit set revision = revision + 1 where id in (new.from_unit, new.to_unit);
|
|
479
|
+
end;
|
|
480
|
+
create trigger unit_rev_replacement_u after update on unit_replacement begin
|
|
481
|
+
update unit set revision = revision + 1 where id in (new.from_unit, new.to_unit);
|
|
482
|
+
end;
|
|
483
|
+
create trigger unit_replacement_gap_frozen before update on unit_replacement_gap begin
|
|
484
|
+
select raise(abort, 'a gap in replacement history is never changed');
|
|
485
|
+
end;
|
|
486
|
+
create trigger unit_state_rules before insert on unit_state begin
|
|
487
|
+
select raise(abort, 'the first state of a unit is candidate, from no state')
|
|
488
|
+
where not exists (select 1 from unit_state where unit_id = new.unit_id)
|
|
489
|
+
and (new.from_state is not null or new.to_state <> 'candidate');
|
|
490
|
+
select raise(abort, 'from_state must be the current lifecycle')
|
|
491
|
+
where new.from_state is not (select lifecycle from unit where id = new.unit_id)
|
|
492
|
+
and exists (select 1 from unit_state where unit_id = new.unit_id);
|
|
493
|
+
select raise(abort, 'not a lifecycle change a unit can make: withdrawn is final, and a superseded unit comes back only once nothing replaces it')
|
|
494
|
+
where exists (select 1 from unit_state where unit_id = new.unit_id) and not (
|
|
495
|
+
(new.from_state = 'candidate' and new.to_state in ('active', 'superseded', 'withdrawn'))
|
|
496
|
+
or (new.from_state = 'active' and new.to_state in ('candidate', 'superseded', 'withdrawn'))
|
|
497
|
+
or (new.from_state = 'superseded' and new.to_state in ('candidate', 'active')
|
|
498
|
+
and not exists (select 1 from unit_replacement where to_unit = new.unit_id and ended_at is null)));
|
|
499
|
+
select raise(abort, 'a quarantined or unsourced unit cannot become active')
|
|
500
|
+
where new.to_state = 'active' and exists (select 1 from unit where id = new.unit_id and (extraction <> 'supported' or unsourced = 1));
|
|
501
|
+
select raise(abort, (select missing from unit_support where unit_id = new.unit_id))
|
|
502
|
+
where new.to_state = 'active' and (select missing from unit_support where unit_id = new.unit_id) is not null;
|
|
503
|
+
-- A reconsider condition is the owner's: each needs a quote of the owner before the unit first becomes active. A quote retracted or
|
|
504
|
+
-- forgotten later leaves the unit as it was (it may come back to active), and readers show the condition as unsupported
|
|
505
|
+
select raise(abort, 'a reconsider condition needs a quote of the owner')
|
|
506
|
+
where new.to_state = 'active' and new.forget_id is null
|
|
507
|
+
and not exists (select 1 from unit_state where unit_id = new.unit_id and to_state = 'active') and exists (select 1 from unit_option o where o.unit_id = new.unit_id
|
|
508
|
+
and o.reconsider_when is not null and not exists (select 1 from unit_evidence e join source s on s.id = e.source_id
|
|
509
|
+
where e.option_id = o.id and e.role = 'reconsiders' and s.author_kind = 'owner'));
|
|
510
|
+
select raise(abort, 'superseded needs a replacement in effect into it')
|
|
511
|
+
where new.to_state = 'superseded' and not exists (select 1 from unit_replacement where to_unit = new.unit_id and ended_at is null);
|
|
512
|
+
select raise(abort, 'a unit something replaces is superseded, not active')
|
|
513
|
+
where new.to_state = 'active' and exists (select 1 from unit_replacement where to_unit = new.unit_id and ended_at is null);
|
|
514
|
+
end;
|
|
515
|
+
create trigger unit_state_append_only before update on unit_state
|
|
516
|
+
when not (new.source_id is null and old.source_id is not null and not exists (select 1 from source where id = old.source_id)
|
|
517
|
+
and new.id is old.id and new.unit_id is old.unit_id and new.from_state is old.from_state and new.to_state is old.to_state
|
|
518
|
+
and new.at is old.at and new.reason is old.reason and new.run_id is old.run_id and new.forget_id is old.forget_id) begin
|
|
519
|
+
select raise(abort, 'state history is append-only');
|
|
520
|
+
end;
|
|
521
|
+
create trigger unit_state_no_delete before delete on unit_state when exists (select 1 from unit where id = old.unit_id) begin
|
|
522
|
+
select raise(abort, 'state history is append-only');
|
|
523
|
+
end;
|
|
524
|
+
create trigger unit_state_apply after insert on unit_state begin
|
|
525
|
+
update unit set lifecycle = new.to_state, revision = revision + 1 where id = new.unit_id;
|
|
526
|
+
end;
|
|
527
|
+
create trigger unit_anchor_frozen before update on unit_anchor begin
|
|
528
|
+
select raise(abort, 'anchors are replaced, not edited; retirement happens once')
|
|
529
|
+
where new.unit_id is not old.unit_id or new.path is not old.path or new.symbol is not old.symbol or new.commit_sha is not old.commit_sha
|
|
530
|
+
or new.line_start is not old.line_start or new.line_end is not old.line_end or new.excerpt is not old.excerpt or new.role is not old.role
|
|
531
|
+
or new.edit_observation_id is not old.edit_observation_id or new.run_id is not old.run_id or new.added_at is not old.added_at
|
|
532
|
+
or old.retired_at is not null or new.retired_at is null;
|
|
533
|
+
select raise(abort, 'an anchor is replaced by another anchor of the same record')
|
|
534
|
+
where new.replaced_by is not null and (new.replaced_by = new.id
|
|
535
|
+
or not exists (select 1 from unit_anchor where id = new.replaced_by and unit_id = new.unit_id));
|
|
536
|
+
end;
|
|
537
|
+
create trigger unit_anchor_no_delete before delete on unit_anchor when exists (select 1 from unit where id = old.unit_id) begin
|
|
538
|
+
select raise(abort, 'anchors are retired, never deleted');
|
|
539
|
+
end;
|
|
540
|
+
create view agent_ineligible_source as
|
|
541
|
+
select s.id as source_id from source s join session se on se.id = s.session_id
|
|
542
|
+
where s.kind = 'session_message' and s.author_kind = 'assistant' and (
|
|
543
|
+
-- A reply with no turn cannot be placed apart from a turn that ran a record tool
|
|
544
|
+
s.turn_id is null
|
|
545
|
+
or exists (select 1 from record_call c where c.host = 'codex' and se.host = 'codex' and c.caller_session = se.external_id
|
|
546
|
+
and (c.caller_turn is null or c.caller_turn = s.turn_id))
|
|
547
|
+
-- The hook's row alone counts: the MCP SDK refuses a malformed call before the server can log it
|
|
548
|
+
or exists (select 1 from tool_call_observation o where o.host = 'claude-code' and se.host = 'claude-code'
|
|
549
|
+
and o.session_external = se.external_id
|
|
550
|
+
-- An observation with no turn places the call only in time, like an unplaced call: replies from then on
|
|
551
|
+
and (o.turn_id = s.turn_id or (o.turn_id is null and s.created_at >= o.observed_at)))
|
|
552
|
+
or exists (select 1 from record_call c where c.project_id = se.project_id and (c.host is null or c.host = se.host)
|
|
553
|
+
and (c.host is null or (c.host = 'codex' and c.caller_session is null)
|
|
554
|
+
or (c.host = 'claude-code' and not exists (select 1 from tool_call_observation o where o.host = 'claude-code' and o.tool_use_id = c.tool_use_id)))
|
|
555
|
+
and s.created_at >= c.called_at));
|
|
556
|
+
create view unit_support as
|
|
557
|
+
select u.id as unit_id, case
|
|
558
|
+
when u.kind in ('decision', 'constraint') and (
|
|
559
|
+
not exists (select 1 from unit_evidence e where e.unit_id = u.id and e.option_id is null and e.retracted_at is null)
|
|
560
|
+
or not exists (select 1 from unit_adoption a where a.unit_id = u.id and a.retracted_at is null
|
|
561
|
+
and (a.route <> 'agent' or exists (select 1 from unit_evidence e where e.unit_id = u.id and e.option_id is null and e.role = 'decides'
|
|
562
|
+
and e.source_id = a.source_id and e.span_start = a.span_start and e.span_end = a.span_end and e.retracted_at is null))))
|
|
563
|
+
then 'an active decision or constraint needs unretracted evidence and adoption'
|
|
564
|
+
when u.kind = 'implementation' and not (
|
|
565
|
+
exists (select 1 from unit_evidence e join source s on s.id = e.source_id where e.unit_id = u.id and e.option_id is null
|
|
566
|
+
and e.retracted_at is null and e.role = 'implements' and s.kind in ('commit_message', 'file_excerpt'))
|
|
567
|
+
or exists (select 1 from unit_anchor a where a.unit_id = u.id and a.retired_at is null and a.role = 'evidence'
|
|
568
|
+
and (a.commit_sha is not null or (a.edit_observation_id is not null and exists (select 1 from unit_evidence e
|
|
569
|
+
join source s on s.id = e.source_id join edit_observation o on o.id = a.edit_observation_id
|
|
570
|
+
where e.unit_id = u.id and e.option_id is null and e.retracted_at is null and e.role = 'implements'
|
|
571
|
+
and s.session_id = o.session_id)))))
|
|
572
|
+
then 'an active implementation needs code or commit evidence'
|
|
573
|
+
when u.kind in ('finding', 'dead_end', 'question')
|
|
574
|
+
and not exists (select 1 from unit_evidence e where e.unit_id = u.id and e.option_id is null and e.retracted_at is null)
|
|
575
|
+
then 'an active unit needs unretracted evidence'
|
|
576
|
+
end as missing
|
|
577
|
+
from unit u;
|
|
578
|
+
create trigger unit_alias_terms before insert on unit_alias begin
|
|
579
|
+
select raise(abort, 'an alias set is bound to the words of its unit as they are')
|
|
580
|
+
where new.content_hash is not (select content_hash from unit where id = new.unit_id);
|
|
581
|
+
select raise(abort, 'each alias is a non-empty string of at most 40 characters')
|
|
582
|
+
where exists (select 1 from json_each(new.terms) where type <> 'text' or length(trim(value)) = 0 or length(value) > 40);
|
|
583
|
+
end;
|
|
584
|
+
create trigger unit_alias_frozen before update on unit_alias begin
|
|
585
|
+
select raise(abort, 'alias sets are replaced by a newer set, not edited');
|
|
586
|
+
end;
|
|
587
|
+
create trigger unit_alias_no_delete before delete on unit_alias when exists (select 1 from unit where id = old.unit_id) begin
|
|
588
|
+
select raise(abort, 'alias sets are append-only; write an empty set to clear');
|
|
589
|
+
end;
|
|
590
|
+
create trigger field_def_check before insert on field_def begin
|
|
591
|
+
select raise(abort, 'a field definition, its source, and its run belong to one project')
|
|
592
|
+
where new.project_id is not (select project_id from source where id = new.source_id)
|
|
593
|
+
or new.project_id is not (select project_id from extraction_run where id = new.run_id);
|
|
594
|
+
select raise(abort, 'fields are defined by trace')
|
|
595
|
+
where (select origin from extraction_run where id = new.run_id) is not 'trace';
|
|
596
|
+
select raise(abort, 'a field definition quotes the owner')
|
|
597
|
+
where not exists (select 1 from source where id = new.source_id and author_kind = 'owner');
|
|
598
|
+
select raise(abort, 'field definition span is outside the source text')
|
|
599
|
+
where new.span_end > (select length(cast(text as blob)) from source where id = new.source_id);
|
|
600
|
+
select raise(abort, 'a span starts or ends inside a character')
|
|
601
|
+
where hex(substr((select cast(text as blob) from source where id = new.source_id), new.span_start + 1, 1)) between '80' and 'BF'
|
|
602
|
+
or hex(substr((select cast(text as blob) from source where id = new.source_id), new.span_end + 1, 1)) between '80' and 'BF';
|
|
603
|
+
select raise(abort, 'enum values are distinct non-empty strings of at most 100 characters')
|
|
604
|
+
where new.enum_values is not null and (exists (select 1 from json_each(new.enum_values)
|
|
605
|
+
where type <> 'text' or length(trim(value)) = 0 or length(value) > 100)
|
|
606
|
+
or (select count(distinct value) from json_each(new.enum_values)) <> json_array_length(new.enum_values));
|
|
607
|
+
select raise(abort, 'field kinds are distinct unit kinds')
|
|
608
|
+
where exists (select 1 from json_each(new.kinds) where type <> 'text'
|
|
609
|
+
or value not in ('decision', 'implementation', 'finding', 'dead_end', 'question', 'constraint'))
|
|
610
|
+
or (select count(distinct value) from json_each(new.kinds)) <> json_array_length(new.kinds);
|
|
611
|
+
end;
|
|
612
|
+
create trigger field_def_frozen before update on field_def begin
|
|
613
|
+
select raise(abort, 'field definitions are never rewritten');
|
|
614
|
+
end;
|
|
615
|
+
create trigger field_def_no_delete before delete on field_def
|
|
616
|
+
when exists (select 1 from project where id = old.project_id) and exists (select 1 from source where id = old.source_id) begin
|
|
617
|
+
select raise(abort, 'field definitions are removed only with their quoted source');
|
|
618
|
+
end;
|
|
619
|
+
create trigger unit_field_check before insert on unit_field begin
|
|
620
|
+
select raise(abort, 'a field value, its unit, definition, source, and run belong to one project')
|
|
621
|
+
where (select project_id from unit where id = new.unit_id) is not (select project_id from field_def where id = new.field_def_id)
|
|
622
|
+
or (select project_id from unit where id = new.unit_id) is not (select project_id from source where id = new.source_id)
|
|
623
|
+
or (select project_id from unit where id = new.unit_id) is not (select project_id from extraction_run where id = new.run_id);
|
|
624
|
+
select raise(abort, 'field values are written by trace')
|
|
625
|
+
where (select origin from extraction_run where id = new.run_id) is not 'trace';
|
|
626
|
+
select raise(abort, 'field values are written with the unit, before its first state; record a successor instead')
|
|
627
|
+
where exists (select 1 from unit_state where unit_id = new.unit_id) or exists (select 1 from unit_alias where unit_id = new.unit_id);
|
|
628
|
+
select raise(abort, 'field value span is outside the source text')
|
|
629
|
+
where new.span_end > (select length(cast(text as blob)) from source where id = new.source_id);
|
|
630
|
+
select raise(abort, 'a span starts or ends inside a character')
|
|
631
|
+
where hex(substr((select cast(text as blob) from source where id = new.source_id), new.span_start + 1, 1)) between '80' and 'BF'
|
|
632
|
+
or hex(substr((select cast(text as blob) from source where id = new.source_id), new.span_end + 1, 1)) between '80' and 'BF';
|
|
633
|
+
select raise(abort, 'the field does not apply to this kind of unit')
|
|
634
|
+
where exists (select 1 from field_def d where d.id = new.field_def_id and json_array_length(d.kinds) > 0
|
|
635
|
+
and not exists (select 1 from json_each(d.kinds) j where j.value = (select kind from unit where id = new.unit_id)));
|
|
636
|
+
select raise(abort, 'an integer field value is an optional minus sign and digits')
|
|
637
|
+
where (select type from field_def where id = new.field_def_id) = 'integer'
|
|
638
|
+
and (new.value not glob '[0-9]*' and new.value not glob '-[0-9]*' or substr(new.value, 2) glob '*[^0-9]*');
|
|
639
|
+
select raise(abort, 'a date field value is a valid YYYY-MM-DD date')
|
|
640
|
+
where (select type from field_def where id = new.field_def_id) = 'date'
|
|
641
|
+
and (length(new.value) <> 10 or date(new.value) is not new.value);
|
|
642
|
+
select raise(abort, 'an enum field value is one of its values')
|
|
643
|
+
where (select type from field_def where id = new.field_def_id) = 'enum' and not exists (select 1 from json_each(
|
|
644
|
+
(select enum_values from field_def where id = new.field_def_id)) where value = new.value);
|
|
645
|
+
end;
|
|
646
|
+
create trigger unit_field_frozen before update on unit_field begin
|
|
647
|
+
select raise(abort, 'field values are never rewritten; record a successor');
|
|
648
|
+
end;
|
|
649
|
+
create trigger unit_field_no_delete before delete on unit_field
|
|
650
|
+
when exists (select 1 from unit where id = old.unit_id) and exists (select 1 from source where id = old.source_id)
|
|
651
|
+
and exists (select 1 from field_def where id = old.field_def_id) begin
|
|
652
|
+
select raise(abort, 'field values are removed only with their unit or a quoted source');
|
|
653
|
+
end;
|
|
654
|
+
create trigger unit_evidence_check before insert on unit_evidence begin
|
|
655
|
+
select raise(abort, 'evidence and unit belong to different projects')
|
|
656
|
+
where (select project_id from unit where id = new.unit_id) is not (select project_id from source where id = new.source_id)
|
|
657
|
+
or (select project_id from unit where id = new.unit_id) is not (select project_id from extraction_run where id = new.run_id);
|
|
658
|
+
select raise(abort, 'evidence span is outside the source text')
|
|
659
|
+
where new.span_end > (select length(cast(text as blob)) from source where id = new.source_id);
|
|
660
|
+
-- A retraction cites the owner's words, which only the retraction's own update checks: a row is never written already retracted
|
|
661
|
+
select raise(abort, 'evidence is written live, then retracted')
|
|
662
|
+
where new.retracted_at is not null or new.retraction_source_id is not null;
|
|
663
|
+
select raise(abort, 'a span starts or ends inside a character')
|
|
664
|
+
where hex(substr((select cast(text as blob) from source where id = new.source_id), new.span_start + 1, 1)) between '80' and 'BF'
|
|
665
|
+
or hex(substr((select cast(text as blob) from source where id = new.source_id), new.span_end + 1, 1)) between '80' and 'BF';
|
|
666
|
+
select raise(abort, 'a reported speaker is the owner reporting someone else, so it must cite an owner session message')
|
|
667
|
+
where new.reported_speaker is not null and (trim(new.reported_speaker) = '' or not exists (select 1 from source
|
|
668
|
+
where id = new.source_id and kind = 'session_message' and author_kind = 'owner'));
|
|
669
|
+
select raise(abort, 'reconsiders quotes the owner on a rejected option that has a reconsider condition')
|
|
670
|
+
where new.role = 'reconsiders' and (new.option_id is null
|
|
671
|
+
or not exists (select 1 from unit_option where id = new.option_id and reconsider_when is not null)
|
|
672
|
+
or not exists (select 1 from source where id = new.source_id and author_kind = 'owner'));
|
|
673
|
+
-- The AI stating its own choice in a reply: never a question it asked (AskUserQuestion's questions are recorded as its message)
|
|
674
|
+
select raise(abort, 'decides quotes the AI choosing in its own reply, never a question it asked or an option')
|
|
675
|
+
where new.role = 'decides' and (new.option_id is not null or not exists (select 1 from source where id = new.source_id
|
|
676
|
+
and kind = 'session_message' and author_kind = 'assistant' and external_id not glob '*:ask:*:q:*'));
|
|
677
|
+
end;
|
|
678
|
+
create trigger unit_evidence_retract before update on unit_evidence begin
|
|
679
|
+
select raise(abort, 'evidence is only ever retracted, once')
|
|
680
|
+
where old.retracted_at is not null or new.unit_id is not old.unit_id or new.option_id is not old.option_id
|
|
681
|
+
or new.source_id is not old.source_id or new.span_start is not old.span_start or new.span_end is not old.span_end
|
|
682
|
+
or new.role is not old.role or new.reported_speaker is not old.reported_speaker or new.run_id is not old.run_id
|
|
683
|
+
or new.added_at is not old.added_at or new.retracted_at is null;
|
|
684
|
+
select raise(abort, 'the retraction must cite an owner span of the same project')
|
|
685
|
+
where not exists (select 1 from source s where s.id = new.retraction_source_id and s.author_kind = 'owner'
|
|
686
|
+
and s.project_id = (select project_id from unit where id = new.unit_id)
|
|
687
|
+
and new.retraction_span_end <= length(cast(s.text as blob)));
|
|
688
|
+
select raise(abort, 'a span starts or ends inside a character')
|
|
689
|
+
where hex(substr((select cast(text as blob) from source where id = new.retraction_source_id), new.retraction_span_start + 1, 1)) between '80' and 'BF'
|
|
690
|
+
or hex(substr((select cast(text as blob) from source where id = new.retraction_source_id), new.retraction_span_end + 1, 1)) between '80' and 'BF';
|
|
691
|
+
end;
|
|
692
|
+
create trigger unit_evidence_no_delete before delete on unit_evidence
|
|
693
|
+
when exists (select 1 from unit where id = old.unit_id) and exists (select 1 from source where id = old.source_id)
|
|
694
|
+
and not (old.retracted_at is not null and not exists (select 1 from source where id = old.retraction_source_id)) begin
|
|
695
|
+
select raise(abort, 'evidence is retracted, never deleted');
|
|
696
|
+
end;
|
|
697
|
+
create trigger unit_adoption_no_delete before delete on unit_adoption
|
|
698
|
+
when exists (select 1 from unit where id = old.unit_id) and exists (select 1 from source where id = old.source_id)
|
|
699
|
+
and not (old.retracted_at is not null and not exists (select 1 from source where id = old.retraction_source_id)) begin
|
|
700
|
+
select raise(abort, 'adoption is retracted, never deleted');
|
|
701
|
+
end;
|
|
702
|
+
create trigger unit_adoption_check before insert on unit_adoption begin
|
|
703
|
+
select raise(abort, 'adoption and unit belong to different projects')
|
|
704
|
+
where (select project_id from unit where id = new.unit_id) is not (select project_id from source where id = new.source_id)
|
|
705
|
+
or (select project_id from unit where id = new.unit_id) is not (select project_id from extraction_run where id = new.run_id);
|
|
706
|
+
select raise(abort, 'adoption span is outside the source text')
|
|
707
|
+
where new.span_end > (select length(cast(text as blob)) from source where id = new.source_id);
|
|
708
|
+
select raise(abort, 'adoption is written live, then retracted')
|
|
709
|
+
where new.retracted_at is not null or new.retraction_source_id is not null;
|
|
710
|
+
select raise(abort, 'a span starts or ends inside a character')
|
|
711
|
+
where hex(substr((select cast(text as blob) from source where id = new.source_id), new.span_start + 1, 1)) between '80' and 'BF'
|
|
712
|
+
or hex(substr((select cast(text as blob) from source where id = new.source_id), new.span_end + 1, 1)) between '80' and 'BF';
|
|
713
|
+
end;
|
|
714
|
+
create trigger unit_adoption_retract before update on unit_adoption begin
|
|
715
|
+
select raise(abort, 'adoption is only ever retracted, once')
|
|
716
|
+
where old.retracted_at is not null or new.unit_id is not old.unit_id or new.route is not old.route
|
|
717
|
+
or new.source_id is not old.source_id or new.span_start is not old.span_start or new.span_end is not old.span_end
|
|
718
|
+
or new.run_id is not old.run_id or new.added_at is not old.added_at or new.retracted_at is null;
|
|
719
|
+
select raise(abort, 'the retraction must cite an owner span of the same project')
|
|
720
|
+
where not exists (select 1 from source s where s.id = new.retraction_source_id and s.author_kind = 'owner'
|
|
721
|
+
and s.project_id = (select project_id from unit where id = new.unit_id)
|
|
722
|
+
and new.retraction_span_end <= length(cast(s.text as blob)));
|
|
723
|
+
select raise(abort, 'a span starts or ends inside a character')
|
|
724
|
+
where hex(substr((select cast(text as blob) from source where id = new.retraction_source_id), new.retraction_span_start + 1, 1)) between '80' and 'BF'
|
|
725
|
+
or hex(substr((select cast(text as blob) from source where id = new.retraction_source_id), new.retraction_span_end + 1, 1)) between '80' and 'BF';
|
|
726
|
+
end;
|
|
727
|
+
create trigger unit_link_check before insert on unit_link begin
|
|
728
|
+
select raise(abort, 'linked units belong to different projects')
|
|
729
|
+
where (select project_id from unit where id = new.from_unit) is not (select project_id from unit where id = new.to_unit)
|
|
730
|
+
or (select project_id from unit where id = new.from_unit) is not (select project_id from extraction_run where id = new.run_id);
|
|
731
|
+
select raise(abort, 'a record supersedes one of its own kind; a decision and a constraint can replace each other')
|
|
732
|
+
where new.kind = 'supersedes' and not exists (select 1 from unit a join unit b on b.id = new.to_unit where a.id = new.from_unit
|
|
733
|
+
and (a.kind = b.kind or (a.kind in ('decision', 'constraint') and b.kind in ('decision', 'constraint'))));
|
|
734
|
+
end;
|
|
735
|
+
create trigger unit_state_project before insert on unit_state begin
|
|
736
|
+
select raise(abort, 'a state comes after its unit was created')
|
|
737
|
+
where new.at < (select created_at from unit where id = new.unit_id);
|
|
738
|
+
select raise(abort, 'state and unit belong to different projects')
|
|
739
|
+
where (new.run_id is not null
|
|
740
|
+
and (select project_id from unit where id = new.unit_id) is not (select project_id from extraction_run where id = new.run_id))
|
|
741
|
+
or (new.forget_id is not null
|
|
742
|
+
and (select project_id from unit where id = new.unit_id) is not (select project_id from forget_batch where id = new.forget_id))
|
|
743
|
+
or (new.source_id is not null
|
|
744
|
+
and (select project_id from unit where id = new.unit_id) is not (select project_id from source where id = new.source_id));
|
|
745
|
+
end;
|
|
746
|
+
create trigger unit_anchor_project before insert on unit_anchor begin
|
|
747
|
+
select raise(abort, 'an anchor is replaced by another anchor of the same record')
|
|
748
|
+
where new.replaced_by is not null and not exists (select 1 from unit_anchor where id = new.replaced_by and unit_id = new.unit_id);
|
|
749
|
+
select raise(abort, 'anchor and unit belong to different projects')
|
|
750
|
+
where (select project_id from unit where id = new.unit_id) is not (select project_id from extraction_run where id = new.run_id);
|
|
751
|
+
select raise(abort, 'the edit observation must be of this path in a session of the same project')
|
|
752
|
+
where new.edit_observation_id is not null and not exists (select 1 from edit_observation o join session s on s.id = o.session_id
|
|
753
|
+
where o.id = new.edit_observation_id and o.path = new.path and s.project_id = (select project_id from unit where id = new.unit_id));
|
|
754
|
+
end;
|
|
755
|
+
create trigger unit_alias_project before insert on unit_alias begin
|
|
756
|
+
select raise(abort, 'alias and unit belong to different projects')
|
|
757
|
+
where (select project_id from unit where id = new.unit_id) is not (select project_id from extraction_run where id = new.run_id);
|
|
758
|
+
end;
|
|
759
|
+
create trigger source_processing_project before insert on source_processing begin
|
|
760
|
+
select raise(abort, 'source and run belong to different projects')
|
|
761
|
+
where (select project_id from source where id = new.source_id) is not (select project_id from extraction_run where id = new.run_id);
|
|
762
|
+
end;
|
|
763
|
+
create trigger unit_rev_evidence_i after insert on unit_evidence begin update unit set revision = revision + 1 where id = new.unit_id; end;
|
|
764
|
+
create trigger unit_rev_evidence_u after update on unit_evidence begin update unit set revision = revision + 1 where id = new.unit_id; end;
|
|
765
|
+
create trigger unit_rev_adoption_i after insert on unit_adoption begin update unit set revision = revision + 1 where id = new.unit_id; end;
|
|
766
|
+
create trigger unit_rev_adoption_u after update on unit_adoption begin update unit set revision = revision + 1 where id = new.unit_id; end;
|
|
767
|
+
create trigger unit_rev_evidence_d after delete on unit_evidence when exists (select 1 from unit where id = old.unit_id) begin
|
|
768
|
+
update unit set revision = revision + 1 where id = old.unit_id;
|
|
769
|
+
end;
|
|
770
|
+
create trigger unit_rev_adoption_d after delete on unit_adoption when exists (select 1 from unit where id = old.unit_id) begin
|
|
771
|
+
update unit set revision = revision + 1 where id = old.unit_id;
|
|
772
|
+
end;
|
|
773
|
+
create trigger unit_rev_link_i after insert on unit_link begin
|
|
774
|
+
update unit set revision = revision + 1 where id in (new.from_unit, new.to_unit);
|
|
775
|
+
end;
|
|
776
|
+
create trigger unit_rev_link_u after update on unit_link begin
|
|
777
|
+
update unit set revision = revision + 1 where id in (new.from_unit, new.to_unit);
|
|
778
|
+
end;
|
|
779
|
+
create trigger unit_rev_anchor_i after insert on unit_anchor begin update unit set revision = revision + 1 where id = new.unit_id; end;
|
|
780
|
+
create trigger unit_rev_anchor_u after update on unit_anchor begin update unit set revision = revision + 1 where id = new.unit_id; end;
|
|
781
|
+
create trigger unit_rev_alias_i after insert on unit_alias begin update unit set revision = revision + 1 where id = new.unit_id; end;
|
|
782
|
+
create trigger unit_rev_field_i after insert on unit_field begin update unit set revision = revision + 1 where id = new.unit_id; end;
|
|
783
|
+
create trigger unit_rev_field_d after delete on unit_field when exists (select 1 from unit where id = old.unit_id) begin
|
|
784
|
+
update unit set revision = revision + 1 where id = old.unit_id;
|
|
785
|
+
end;
|
|
786
|
+
create view unit_search_text as
|
|
787
|
+
select u.id,
|
|
788
|
+
sphica_terms(u.text || char(10) || coalesce(u.why, '') || char(10) || coalesce(u.scope_note, '') || char(10)
|
|
789
|
+
|| coalesce(u.revisit_when, '') || char(10)
|
|
790
|
+
|| coalesce((select group_concat(o.text || ' ' || coalesce(o.why, '') || ' ' || coalesce(o.reconsider_when, ''), char(10))
|
|
791
|
+
from (select text, why, reconsider_when from unit_option where unit_id = u.id order by position) o), '') || char(10)
|
|
792
|
+
|| coalesce((select group_concat(f.name || ' ' || f.value, char(10))
|
|
793
|
+
from (select d.name, v.value from unit_field v join field_def d on d.id = v.field_def_id where v.unit_id = u.id order by v.id) f), ''))
|
|
794
|
+
as body,
|
|
795
|
+
sphica_terms(coalesce((select group_concat(a.path || ' ' || coalesce(a.symbol, ''), char(10))
|
|
796
|
+
from (select path, symbol from unit_anchor where unit_id = u.id and retired_at is null order by id) a), '')) as ident,
|
|
797
|
+
sphica_terms(coalesce((select group_concat(j.value, ' ')
|
|
798
|
+
from json_each((select terms from unit_alias where unit_id = u.id and content_hash = u.content_hash order by id desc limit 1)) j), ''))
|
|
799
|
+
as alias
|
|
800
|
+
from unit u;
|
|
801
|
+
create trigger unit_fts_ai after insert on unit begin
|
|
802
|
+
insert into unit_fts (rowid, body, ident, alias) select id, body, ident, alias from unit_search_text where id = new.id;
|
|
803
|
+
end;
|
|
804
|
+
create trigger unit_fts_ad after delete on unit begin
|
|
805
|
+
delete from unit_fts where rowid = old.id;
|
|
806
|
+
end;
|
|
807
|
+
create trigger unit_fts_option_i after insert on unit_option begin
|
|
808
|
+
delete from unit_fts where rowid = new.unit_id;
|
|
809
|
+
insert into unit_fts (rowid, body, ident, alias) select id, body, ident, alias from unit_search_text where id = new.unit_id;
|
|
810
|
+
end;
|
|
811
|
+
create trigger unit_fts_anchor_i after insert on unit_anchor begin
|
|
812
|
+
delete from unit_fts where rowid = new.unit_id;
|
|
813
|
+
insert into unit_fts (rowid, body, ident, alias) select id, body, ident, alias from unit_search_text where id = new.unit_id;
|
|
814
|
+
end;
|
|
815
|
+
create trigger unit_fts_anchor_u after update on unit_anchor begin
|
|
816
|
+
delete from unit_fts where rowid = new.unit_id;
|
|
817
|
+
insert into unit_fts (rowid, body, ident, alias) select id, body, ident, alias from unit_search_text where id = new.unit_id;
|
|
818
|
+
end;
|
|
819
|
+
create trigger unit_fts_anchor_d after delete on unit_anchor when exists (select 1 from unit where id = old.unit_id) begin
|
|
820
|
+
delete from unit_fts where rowid = old.unit_id;
|
|
821
|
+
insert into unit_fts (rowid, body, ident, alias) select id, body, ident, alias from unit_search_text where id = old.unit_id;
|
|
822
|
+
end;
|
|
823
|
+
create trigger unit_fts_alias_i after insert on unit_alias begin
|
|
824
|
+
delete from unit_fts where rowid = new.unit_id;
|
|
825
|
+
insert into unit_fts (rowid, body, ident, alias) select id, body, ident, alias from unit_search_text where id = new.unit_id;
|
|
826
|
+
end;
|
|
827
|
+
create trigger unit_fts_alias_d after delete on unit_alias when exists (select 1 from unit where id = old.unit_id) begin
|
|
828
|
+
delete from unit_fts where rowid = old.unit_id;
|
|
829
|
+
insert into unit_fts (rowid, body, ident, alias) select id, body, ident, alias from unit_search_text where id = old.unit_id;
|
|
830
|
+
end;
|
|
831
|
+
create trigger unit_fts_field_i after insert on unit_field begin
|
|
832
|
+
delete from unit_fts where rowid = new.unit_id;
|
|
833
|
+
insert into unit_fts (rowid, body, ident, alias) select id, body, ident, alias from unit_search_text where id = new.unit_id;
|
|
834
|
+
end;
|
|
835
|
+
create trigger unit_fts_field_d after delete on unit_field when exists (select 1 from unit where id = old.unit_id) begin
|
|
836
|
+
delete from unit_fts where rowid = old.unit_id;
|
|
837
|
+
insert into unit_fts (rowid, body, ident, alias) select id, body, ident, alias from unit_search_text where id = old.unit_id;
|
|
838
|
+
end;
|
|
839
|
+
create trigger delivery_ad after delete on delivery begin
|
|
840
|
+
delete from delivery_unit where delivery_id = old.id;
|
|
841
|
+
end;
|
|
842
|
+
create view ingest_source as
|
|
843
|
+
select project_id, kind, artifact, external_id, revision, author_kind, author_login, author_external_id, author_association,
|
|
844
|
+
parent_external_id, event_kind, url, created_at, available_at, captured_at, text, truncated, redacted, original_bytes,
|
|
845
|
+
content_hash, path, line_start, line_end, diff_hunk, commit_sha, blob_sha, indexed
|
|
846
|
+
from source where session_id is null;
|
|
847
|
+
create trigger ingest_source_insert instead of insert on ingest_source begin
|
|
848
|
+
insert into source (project_id, kind, artifact, external_id, revision, author_kind, author_login, author_external_id, author_association,
|
|
849
|
+
parent_external_id, event_kind, url, created_at, available_at, captured_at, text, truncated, redacted, original_bytes,
|
|
850
|
+
content_hash, path, line_start, line_end, diff_hunk, commit_sha, blob_sha, indexed, session_id, turn_id)
|
|
851
|
+
values (new.project_id, new.kind, new.artifact, new.external_id, new.revision, new.author_kind, new.author_login, new.author_external_id,
|
|
852
|
+
new.author_association, new.parent_external_id, new.event_kind, new.url, new.created_at, new.available_at, new.captured_at,
|
|
853
|
+
new.text, coalesce(new.truncated, 0), coalesce(new.redacted, 0), new.original_bytes, new.content_hash, new.path, new.line_start,
|
|
854
|
+
new.line_end, new.diff_hunk, new.commit_sha, new.blob_sha, new.indexed, null, null);
|
|
855
|
+
end;
|
|
856
|
+
create view capture_session as select id, project_id, host, external_id, branch, started_at from session;
|
|
857
|
+
create trigger capture_session_insert instead of insert on capture_session begin
|
|
858
|
+
select raise(abort, 'the session already exists with different details')
|
|
859
|
+
where exists (select 1 from session where id = new.id and (project_id <> new.project_id or host <> new.host or external_id <> new.external_id));
|
|
860
|
+
insert into session (id, project_id, host, external_id, branch, started_at)
|
|
861
|
+
select new.id, new.project_id, new.host, new.external_id, new.branch, new.started_at
|
|
862
|
+
where not exists (select 1 from session where id = new.id);
|
|
863
|
+
end;
|
|
864
|
+
create view capture_message as
|
|
865
|
+
select external_id, session_id, turn_id, author_kind as speaker, created_at, captured_at, text, truncated, redacted, original_bytes,
|
|
866
|
+
content_hash from source where kind = 'session_message';
|
|
867
|
+
create trigger capture_message_insert instead of insert on capture_message begin
|
|
868
|
+
select raise(abort, 'unknown session') where not exists (select 1 from session where id = new.session_id);
|
|
869
|
+
select raise(abort, 'speaker must be owner or assistant') where new.speaker not in ('owner', 'assistant');
|
|
870
|
+
select raise(abort, 'the message already exists with different content')
|
|
871
|
+
where exists (select 1 from source where kind = 'session_message' and session_id = new.session_id and external_id = new.external_id
|
|
872
|
+
and (text is not new.text or author_kind is not new.speaker or turn_id is not new.turn_id or created_at is not new.created_at
|
|
873
|
+
or truncated is not new.truncated or redacted is not new.redacted or original_bytes is not new.original_bytes
|
|
874
|
+
or content_hash is not new.content_hash));
|
|
875
|
+
insert into source (project_id, kind, artifact, external_id, revision, session_id, turn_id, author_kind, created_at, available_at,
|
|
876
|
+
captured_at, text, truncated, redacted, original_bytes, content_hash, indexed)
|
|
877
|
+
select s.project_id, 'session_message', 'session:' || s.id, new.external_id, 1, s.id, new.turn_id, new.speaker, new.created_at,
|
|
878
|
+
new.created_at, new.captured_at, new.text, new.truncated, new.redacted, new.original_bytes, new.content_hash,
|
|
879
|
+
new.speaker = 'owner'
|
|
880
|
+
from session s where s.id = new.session_id
|
|
881
|
+
and not exists (select 1 from source where kind = 'session_message' and session_id = new.session_id and external_id = new.external_id)
|
|
882
|
+
and not exists (select 1 from source_forgotten f where f.project_id = s.project_id and f.artifact = 'session:' || s.id
|
|
883
|
+
and f.kind = 'session_message' and f.external_id = new.external_id and f.content_hash = new.content_hash);
|
|
884
|
+
end;
|
|
885
|
+
create view capture_edit as select session_id, turn_id, tool_event_id, path, via, observed_at from edit_observation;
|
|
886
|
+
create trigger capture_edit_insert instead of insert on capture_edit begin
|
|
887
|
+
insert into edit_observation (session_id, turn_id, tool_event_id, path, via, observed_at)
|
|
888
|
+
select new.session_id, new.turn_id, new.tool_event_id, new.path, new.via, new.observed_at
|
|
889
|
+
where exists (select 1 from session where id = new.session_id) on conflict do nothing;
|
|
890
|
+
end;
|
|
891
|
+
create view capture_tool_call as
|
|
892
|
+
select host, session_external, turn_id, tool_use_id, tool_name, owner_turn, observed_at from tool_call_observation;
|
|
893
|
+
create trigger capture_tool_call_insert instead of insert on capture_tool_call begin
|
|
894
|
+
insert into tool_call_observation (host, session_external, turn_id, tool_use_id, tool_name, owner_turn, observed_at)
|
|
895
|
+
values (new.host, new.session_external, new.turn_id, new.tool_use_id, new.tool_name, new.owner_turn, new.observed_at)
|
|
896
|
+
on conflict do nothing;
|
|
897
|
+
end;
|
|
898
|
+
create view capture_delivery as
|
|
899
|
+
select session_id, event, outcome, reason, path, eligible, omitted, chars, at, null as units from delivery;
|
|
900
|
+
create trigger capture_delivery_insert instead of insert on capture_delivery begin
|
|
901
|
+
select raise(abort, 'the unit and the delivered session belong to different projects')
|
|
902
|
+
where exists (select 1 from json_each(coalesce(new.units, '[]')) j
|
|
903
|
+
where (select project_id from unit where id = j.value) is not (select project_id from session where id = new.session_id));
|
|
904
|
+
insert into delivery (session_id, event, outcome, reason, path, eligible, omitted, chars, at)
|
|
905
|
+
values (new.session_id, new.event, new.outcome, new.reason, new.path, coalesce(new.eligible, 0), coalesce(new.omitted, 0),
|
|
906
|
+
coalesce(new.chars, 0), new.at);
|
|
907
|
+
insert into delivery_unit (delivery_id, unit_id)
|
|
908
|
+
select last_insert_rowid(), j.value from json_each(coalesce(new.units, '[]')) j where true on conflict do nothing;
|
|
909
|
+
end;
|
|
910
|
+
create view capture_delivery_scoped as
|
|
911
|
+
select session_id, agent_id, event, outcome, reason, path, eligible, omitted, chars, at, null as units from delivery;
|
|
912
|
+
create trigger capture_delivery_scoped_insert instead of insert on capture_delivery_scoped begin
|
|
913
|
+
select raise(abort, 'the unit and the delivered session belong to different projects')
|
|
914
|
+
where exists (select 1 from json_each(coalesce(new.units, '[]')) j
|
|
915
|
+
where (select project_id from unit where id = j.value) is not (select project_id from session where id = new.session_id));
|
|
916
|
+
insert into delivery (session_id, agent_id, event, outcome, reason, path, eligible, omitted, chars, at)
|
|
917
|
+
values (new.session_id, new.agent_id, new.event, new.outcome, new.reason, new.path, coalesce(new.eligible, 0),
|
|
918
|
+
coalesce(new.omitted, 0), coalesce(new.chars, 0), new.at);
|
|
919
|
+
insert into delivery_unit (delivery_id, unit_id)
|
|
920
|
+
select last_insert_rowid(), j.value from json_each(coalesce(new.units, '[]')) j where true on conflict do nothing;
|
|
921
|
+
end;
|
|
922
|
+
create view capture_delivery_prune as select null as cutoff, null as session_id;
|
|
923
|
+
create trigger capture_delivery_prune_insert instead of insert on capture_delivery_prune begin
|
|
924
|
+
delete from delivery where session_id = new.session_id
|
|
925
|
+
and not exists (select 1 from delivery n where n.session_id = new.session_id and n.at >= new.cutoff);
|
|
926
|
+
delete from delivery where id in (select d.id from delivery d where d.at < new.cutoff
|
|
927
|
+
and (d.session_id is null or not exists (select 1 from delivery n where n.session_id = d.session_id and n.at >= new.cutoff))
|
|
928
|
+
order by d.at, d.id limit 200);
|
|
929
|
+
end;
|
|
930
|
+
|
|
931
|
+
pragma user_version = 10;
|