sphica 0.6.13 → 0.6.15

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/db/schema.sql CHANGED
@@ -3,13 +3,21 @@
3
3
  -- A database of another generation is refused without being changed.
4
4
  --
5
5
  -- Four boundaries:
6
- -- captured sources session, source, artifact_link, edit_observation, external_reference: what was said or written, never rewritten
6
+ -- captured sources session, source, artifact_link, edit_observation: what was said or written, never rewritten
7
7
  -- (the owner can forget chosen sources: forget_batch and source_forgotten keep what was removed, without its text)
8
8
  -- extracted units unit and its option, evidence, adoption, link, state, anchor, alias tables: what was decided or implemented
9
9
  -- processing extraction_run, source_processing: what has been looked at and saved, so gaps are counted
10
10
  -- work and delivery work, delivery, delivery_unit: the current work status and what the hooks injected
11
11
  -- Every table is STRICT and every primary key is not null. Times are ISO 8601 UTC (`Date#toISOString()`); `strftime(...) is column` rejects others.
12
+ -- Paths (a source's, an edit observation's, an anchor's) are repository-relative with forward slashes, in one form, so one place
13
+ -- has one spelling: the three CHECKs are the same expression (scripts/check-pairs.mjs compares them).
12
14
  -- Byte offsets are into the UTF-8 bytes of source.text. Project consistency across tables is enforced by triggers, not only by code.
15
+ -- What a delete takes with it: a row goes with what it belongs to, and a row citing a source goes with that source (cascade; the
16
+ -- no-delete triggers refuse removing such a row on its own). A column naming where a row came from (run_id, forget_id,
17
+ -- edit_observation_id) takes no action: its target goes only with the whole project, removed in the same statement. A session that
18
+ -- records cite is not deleted by hand: forget its messages instead, which judges the records again.
19
+ -- Every foreign key is led by an index on its own columns (a partial one where the column can be null), so removing or moving a parent
20
+ -- row looks its children up instead of scanning them.
13
21
 
14
22
  create table sphica_generation (generation integer not null check (generation = 2)) strict;
15
23
  insert into sphica_generation values (2);
@@ -65,11 +73,12 @@ create table source (
65
73
  author_login text,
66
74
  author_external_id text,
67
75
  author_association text,
68
- -- The comment or thread this replies to, or the thread a resolution event closed
76
+ -- The comment or thread this replies to
69
77
  parent_external_id text,
70
- -- For pr_event: merged | closed | reopened | thread_resolved
71
- event_kind text check (event_kind in ('merged', 'closed', 'reopened', 'thread_resolved')),
72
- url text,
78
+ -- For pr_event: merged (the one event harvest records)
79
+ event_kind text check (event_kind in ('merged')),
80
+ -- Where it lives on the web; any other scheme (javascript:, file:) is never kept, since readers may show it as a link
81
+ url text check (url glob 'https://*' or url glob 'http://*'),
73
82
  created_at text not null check (strftime('%Y-%m-%dT%H:%M:%fZ', created_at) is created_at),
74
83
  available_at text check (strftime('%Y-%m-%dT%H:%M:%fZ', available_at) is available_at),
75
84
  captured_at text not null check (strftime('%Y-%m-%dT%H:%M:%fZ', captured_at) is captured_at),
@@ -79,11 +88,11 @@ create table source (
79
88
  original_bytes integer not null check (original_bytes >= 0),
80
89
  content_hash blob not null check (length(content_hash) = 32),
81
90
  -- Code position of a review comment or a file excerpt: a normalized repository-relative path with forward slashes
82
- path text check (path is null or (path <> '' and path not glob '/*' and path not glob '*[/]..[/]*' and path not glob '..[/]*'
83
- and path not glob '*[/]..' and path <> '..' and path not glob '*\*' and path not glob '[A-Za-z]:*' and path not glob '*//*'
84
- and path not glob './*' and path not glob '*[/].[/]*')),
91
+ path text check (path is null or (path <> '' and path <> '.' and path <> '..' and path not glob '/*' and path not glob '[A-Za-z]:*' and path not glob '*\*'
92
+ and path not glob '*//*' and path not glob './*' and path not glob '../*' and path not glob '*/./*' and path not glob '*/../*'
93
+ and path not glob '*/.' and path not glob '*/..' and path not glob '*[' || char(1) || '-' || char(31) || char(127) || '-' || char(159) || ']*')),
85
94
  line_start integer check (line_start > 0),
86
- line_end integer check (line_end >= line_start),
95
+ line_end integer check (line_end is null or (line_start is not null and line_end >= line_start)),
87
96
  diff_hunk text,
88
97
  commit_sha text check (commit_sha is null or (length(commit_sha) = 40 and commit_sha not glob '*[^0-9a-f]*')),
89
98
  blob_sha text check (blob_sha is null or (length(blob_sha) = 40 and blob_sha not glob '*[^0-9a-f]*')),
@@ -93,6 +102,7 @@ create table source (
93
102
  check ((kind = 'pr_event') = (event_kind is not null)),
94
103
  check (kind <> 'session_message' or author_kind in ('owner', 'assistant')),
95
104
  check (kind = 'session_message' or author_kind <> 'assistant'),
105
+ check (author_kind <> 'assistant' or indexed = 0),
96
106
  check (kind <> 'file_excerpt' or (path is not null and commit_sha is not null and blob_sha is not null
97
107
  and line_start is not null and line_end is not null)),
98
108
  check (truncated = 1 or redacted = 1 or original_bytes = length(cast(text as blob)))
@@ -109,6 +119,17 @@ when new.kind <> 'session_message' and new.author_kind = 'owner'
109
119
  and not exists (select 1 from owner_identity where provider = 'github' and external_id = new.author_external_id) begin
110
120
  select raise(abort, 'owner authorship needs a bound owner identity');
111
121
  end;
122
+ create trigger session_cited before delete on session
123
+ when exists (select 1 from project where id = old.project_id) and (
124
+ exists (select 1 from source s where s.session_id = old.id and (
125
+ exists (select 1 from unit_evidence e where e.source_id = s.id or e.retraction_source_id = s.id)
126
+ or exists (select 1 from unit_adoption a where a.source_id = s.id or a.retraction_source_id = s.id)
127
+ or exists (select 1 from field_def d where d.source_id = s.id)
128
+ or exists (select 1 from unit_field f where f.source_id = s.id)
129
+ or exists (select 1 from unit_state t where t.source_id = s.id)))
130
+ 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
131
+ select raise(abort, 'records cite this session; forget its messages with /sphica:forget rather than deleting it');
132
+ end;
112
133
  create trigger source_session_project before insert on source when new.session_id is not null
113
134
  and not exists (select 1 from session where id = new.session_id and project_id = new.project_id) begin
114
135
  select raise(abort, 'source and session belong to different projects');
@@ -134,24 +155,13 @@ create table artifact_link (
134
155
  primary key (project_id, from_artifact, to_artifact, kind)
135
156
  ) strict;
136
157
 
137
- -- A reference the owner gave that Sphica could not fetch (a meeting-notes URL). It never counts as a fetched source:
138
- -- a claim supported only by it stays unsourced until the text is fetched and checked.
139
- create table external_reference (
140
- id integer primary key autoincrement not null,
141
- project_id integer not null references project (id) on delete cascade,
142
- url text not null check (url <> ''),
143
- owner_source_id integer not null references source (id),
144
- span_start integer not null check (span_start >= 0),
145
- span_end integer not null check (span_end > span_start),
146
- added_at text not null check (strftime('%Y-%m-%dT%H:%M:%fZ', added_at) is added_at)
147
- ) strict;
148
-
149
158
  -- One owner-confirmed deletion of chosen sources. It stands in for an extraction run on the state changes the deletion causes.
150
159
  create table forget_batch (
151
160
  id integer primary key autoincrement not null,
152
161
  project_id integer not null references project (id) on delete cascade,
153
162
  at text not null check (strftime('%Y-%m-%dT%H:%M:%fZ', at) is at)
154
163
  ) strict;
164
+ create index forget_batch_project on forget_batch (project_id);
155
165
 
156
166
  -- A source the owner forgot: its original id and identity, never its text. Capture, harvest, and glean skip an item matching one,
157
167
  -- so the same words are not stored again; changed text is new speech and is stored. source_id has no foreign key: the row is gone.
@@ -166,6 +176,7 @@ create table source_forgotten (
166
176
  content_hash blob not null check (length(content_hash) = 32),
167
177
  batch_id integer not null references forget_batch (id) on delete cascade
168
178
  ) strict;
179
+ create index source_forgotten_batch on source_forgotten (batch_id);
169
180
  -- Not unique: an edited item that returns to earlier text has two revisions with the same hash, and both can be forgotten
170
181
  create index source_forgotten_item on source_forgotten (project_id, artifact, kind, external_id, content_hash);
171
182
 
@@ -175,38 +186,54 @@ create table edit_observation (
175
186
  session_id text not null references session (id) on delete cascade,
176
187
  turn_id text,
177
188
  tool_event_id text,
178
- path text not null check (path <> '' and path not glob '/*' and path not glob '*[/]..[/]*' and path not glob '..[/]*'
179
- and path not glob '*[/]..' and path <> '..' and path not glob '*\*' and path not glob '[A-Za-z]:*'),
189
+ path text not null check (path <> '' and path <> '.' and path <> '..' and path not glob '/*' and path not glob '[A-Za-z]:*' and path not glob '*\*'
190
+ and path not glob '*//*' and path not glob './*' and path not glob '../*' and path not glob '*/./*' and path not glob '*/../*'
191
+ and path not glob '*/.' and path not glob '*/..' and path not glob '*[' || char(1) || '-' || char(31) || char(127) || '-' || char(159) || ']*'),
180
192
  via text not null check (via in ('tool', 'status')),
181
- observed_at text not null check (strftime('%Y-%m-%dT%H:%M:%fZ', observed_at) is observed_at),
182
- unique (session_id, turn_id, path, via)
193
+ observed_at text not null check (strftime('%Y-%m-%dT%H:%M:%fZ', observed_at) is observed_at)
183
194
  ) strict;
195
+ -- One row per path, way, and turn of a session; an observation outside any turn (turn_id null) counts once too
196
+ create unique index edit_observation_once on edit_observation (session_id, coalesce(turn_id, ''), path, via);
184
197
  create index edit_observation_path on edit_observation (path);
185
198
 
186
199
  -- One extraction by trace, harvest, or glean. target names what it read: `session:<uuid>`, `pr:<n>`, or `glean`.
200
+ -- A migration that changes lifecycles records itself as a run too (origin migration, target `revision:<n>`), so each change names where it came from.
187
201
  create table extraction_run (
188
202
  id integer primary key autoincrement not null,
189
203
  project_id integer not null references project (id) on delete cascade,
190
- origin text not null check (origin in ('trace', 'harvest', 'glean')),
204
+ origin text not null check (origin in ('trace', 'harvest', 'glean', 'migration')),
191
205
  target text not null,
192
206
  session_id text references session (id) on delete set null,
193
- status text not null check (status in ('running', 'saved', 'failed', 'capped')),
194
- reason text,
207
+ status text not null check (status in ('running', 'saved')),
195
208
  input_bytes integer check (input_bytes >= 0),
196
209
  -- 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
197
210
  draft_id text unique,
198
211
  started_at text not null check (strftime('%Y-%m-%dT%H:%M:%fZ', started_at) is started_at),
199
212
  finished_at text check (strftime('%Y-%m-%dT%H:%M:%fZ', finished_at) is finished_at),
200
- check (status in ('running', 'saved') or reason is not null)
213
+ check (finished_at >= started_at)
201
214
  ) strict;
215
+ create index extraction_run_project on extraction_run (project_id);
216
+ create index extraction_run_session on extraction_run (session_id) where session_id is not null;
217
+
218
+ -- A run changes once, when it finishes: only a running run takes a status and a finish time. The one other update is the foreign key
219
+ -- action clearing session_id after its session was deleted
220
+ create trigger extraction_run_frozen before update on extraction_run
221
+ 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
222
+ or new.input_bytes is not old.input_bytes or new.draft_id is not old.draft_id
223
+ or new.started_at is not old.started_at
224
+ 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)))
225
+ or ((new.status is not old.status or new.finished_at is not old.finished_at) and old.status <> 'running') begin
226
+ select raise(abort, 'a run changes once, when it finishes');
227
+ end;
202
228
 
203
229
  -- Which sources an extraction looked at, and what came of them
204
230
  create table source_processing (
205
231
  source_id integer not null references source (id) on delete cascade,
206
232
  run_id integer not null references extraction_run (id) on delete cascade,
207
- outcome text not null check (outcome in ('units', 'no_unit', 'failed', 'capped')),
233
+ outcome text not null check (outcome in ('units', 'no_unit')),
208
234
  primary key (source_id, run_id)
209
235
  ) strict;
236
+ create index source_processing_run on source_processing (run_id);
210
237
 
211
238
  -- An extracted unit. Its text is never rewritten: corrections are successors, withdrawals, retractions, and anchor replacements.
212
239
  -- extraction: supported (every evidence span was found in retained text) or quarantined (with reason).
@@ -241,6 +268,9 @@ create table unit (
241
268
  check (unsourced = 0 or lifecycle <> 'active')
242
269
  ) strict;
243
270
  create index unit_live on unit (project_id, lifecycle, kind);
271
+ -- The same words saved twice under two keys are allowed (a rewrite is a successor), and doctor counts them
272
+ create index unit_content on unit (project_id, content_hash);
273
+ create index unit_run on unit (run_id);
244
274
  create trigger unit_insert_candidate before insert on unit when new.lifecycle <> 'candidate' begin
245
275
  select raise(abort, 'units start as candidates');
246
276
  end;
@@ -248,10 +278,15 @@ create trigger unit_run_project before insert on unit
248
278
  when not exists (select 1 from extraction_run where id = new.run_id and project_id = new.project_id) begin
249
279
  select raise(abort, 'unit and run belong to different projects');
250
280
  end;
251
- create trigger unit_text_frozen before update of project_id, key, kind, stance, text, why, scope_note, revisit_when, content_hash, run_id
281
+ -- Everything a unit was saved with is frozen, so a quarantined or unsourced unit cannot be relabelled and then activated
282
+ create trigger unit_text_frozen before update of project_id, key, kind, stance, text, why, scope_note, revisit_when, no_code_surface,
283
+ extraction, extraction_reason, unsourced, run_id, created_at, content_hash
252
284
  on unit begin
253
285
  select raise(abort, 'unit text is never rewritten; record a successor');
254
286
  end;
287
+ create trigger unit_revision_step before update of revision on unit when new.revision is not old.revision + 1 begin
288
+ select raise(abort, 'a unit''s revision rises by one with each change to its relations');
289
+ end;
255
290
  create trigger unit_lifecycle_via_state before update of lifecycle on unit
256
291
  when new.lifecycle is not (select to_state from unit_state where unit_id = new.id order by id desc limit 1) begin
257
292
  select raise(abort, 'lifecycle changes only through unit_state');
@@ -298,7 +333,7 @@ create table unit_evidence (
298
333
  added_at text not null check (strftime('%Y-%m-%dT%H:%M:%fZ', added_at) is added_at),
299
334
  retracted_at text check (strftime('%Y-%m-%dT%H:%M:%fZ', retracted_at) is retracted_at),
300
335
  retraction_reason text,
301
- retraction_source_id integer references source (id),
336
+ retraction_source_id integer references source (id) on delete cascade,
302
337
  retraction_span_start integer,
303
338
  retraction_span_end integer,
304
339
  foreign key (unit_id, option_id) references unit_option (unit_id, id) on delete cascade,
@@ -306,11 +341,16 @@ create table unit_evidence (
306
341
  check ((retracted_at is null) = (retraction_source_id is null)),
307
342
  check ((retraction_source_id is null) = (retraction_span_start is null)),
308
343
  check ((retraction_source_id is null) = (retraction_span_end is null)),
309
- check (retraction_span_end is null or retraction_span_end > retraction_span_start)
344
+ check (retraction_span_end is null or retraction_span_end > retraction_span_start),
345
+ check (retraction_span_start >= 0),
346
+ check (retracted_at >= added_at)
310
347
  ) strict;
311
348
  create unique index unit_evidence_unit_once on unit_evidence (unit_id, source_id, span_start, span_end, role) where option_id is null;
312
349
  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;
313
350
  create index unit_evidence_source on unit_evidence (source_id);
351
+ create index unit_evidence_option on unit_evidence (unit_id, option_id);
352
+ create index unit_evidence_retraction on unit_evidence (retraction_source_id) where retraction_source_id is not null;
353
+ create index unit_evidence_run on unit_evidence (run_id);
314
354
 
315
355
  -- Evidence that the project adopted a decision or constraint. route: owner_statement (an owner-kind source span) or
316
356
  -- explicit (an explicit disposition in a source, such as a maintainer's reply saying it is adopted). A merge or a resolved thread is never adoption.
@@ -325,7 +365,7 @@ create table unit_adoption (
325
365
  added_at text not null check (strftime('%Y-%m-%dT%H:%M:%fZ', added_at) is added_at),
326
366
  retracted_at text check (strftime('%Y-%m-%dT%H:%M:%fZ', retracted_at) is retracted_at),
327
367
  retraction_reason text,
328
- retraction_source_id integer references source (id),
368
+ retraction_source_id integer references source (id) on delete cascade,
329
369
  retraction_span_start integer,
330
370
  retraction_span_end integer,
331
371
  unique (unit_id, source_id, span_start, span_end),
@@ -333,15 +373,20 @@ create table unit_adoption (
333
373
  check ((retracted_at is null) = (retraction_source_id is null)),
334
374
  check ((retraction_source_id is null) = (retraction_span_start is null)),
335
375
  check ((retraction_source_id is null) = (retraction_span_end is null)),
336
- check (retraction_span_end is null or retraction_span_end > retraction_span_start)
376
+ check (retraction_span_end is null or retraction_span_end > retraction_span_start),
377
+ check (retraction_span_start >= 0),
378
+ check (retracted_at >= added_at)
337
379
  ) strict;
380
+ create index unit_adoption_source on unit_adoption (source_id);
381
+ create index unit_adoption_retraction on unit_adoption (retraction_source_id) where retraction_source_id is not null;
382
+ create index unit_adoption_run on unit_adoption (run_id);
338
383
  create trigger unit_adoption_route before insert on unit_adoption begin
339
384
  select raise(abort, 'owner_statement adoption needs an owner-authored source')
340
385
  where new.route = 'owner_statement' and not exists (select 1 from source where id = new.source_id and author_kind = 'owner');
341
386
  select raise(abort, 'explicit adoption needs the owner or a maintainer (OWNER, MEMBER, COLLABORATOR association)')
342
387
  where new.route = 'explicit' and not exists (select 1 from source where id = new.source_id
343
388
  and (author_kind = 'owner' or author_association in ('OWNER', 'MEMBER', 'COLLABORATOR')));
344
- select raise(abort, 'merge and thread resolution events are not adoption')
389
+ select raise(abort, 'a merge is not adoption')
345
390
  where exists (select 1 from source where id = new.source_id and kind = 'pr_event');
346
391
  select raise(abort, 'adoption applies to decisions and constraints')
347
392
  where not exists (select 1 from unit where id = new.unit_id and kind in ('decision', 'constraint'));
@@ -350,7 +395,7 @@ end;
350
395
  create table unit_link (
351
396
  from_unit integer not null references unit (id) on delete cascade,
352
397
  to_unit integer not null references unit (id) on delete cascade,
353
- kind text not null check (kind in ('supersedes', 'implements', 'conflicts')),
398
+ kind text not null check (kind in ('supersedes', 'conflicts')),
354
399
  run_id integer not null references extraction_run (id),
355
400
  added_at text not null check (strftime('%Y-%m-%dT%H:%M:%fZ', added_at) is added_at),
356
401
  -- A conflict stays unresolved (and suppresses automatic delivery of both) until resolved with a reason
@@ -361,6 +406,9 @@ create table unit_link (
361
406
  check ((resolved_at is null) = (resolution is null)),
362
407
  check (kind = 'conflicts' or resolved_at is null)
363
408
  ) strict;
409
+ -- Lookups by the unit a link points at: its successors, and its conflicts from either side
410
+ create index unit_link_to on unit_link (to_unit, kind);
411
+ create index unit_link_run on unit_link (run_id);
364
412
  create trigger unit_link_frozen before update on unit_link begin
365
413
  select raise(abort, 'links are frozen; only an unresolved conflict can be resolved, once')
366
414
  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
@@ -393,36 +441,38 @@ create table unit_state (
393
441
  check ((run_id is null) <> (forget_id is null))
394
442
  ) strict;
395
443
  create index unit_state_order on unit_state (unit_id, id);
444
+ create index unit_state_source on unit_state (source_id) where source_id is not null;
445
+ create index unit_state_run on unit_state (run_id) where run_id is not null;
446
+ create index unit_state_forget on unit_state (forget_id) where forget_id is not null;
396
447
  create trigger unit_state_rules before insert on unit_state begin
448
+ select raise(abort, 'the first state of a unit is candidate, from no state')
449
+ where not exists (select 1 from unit_state where unit_id = new.unit_id)
450
+ and (new.from_state is not null or new.to_state <> 'candidate');
397
451
  select raise(abort, 'from_state must be the current lifecycle')
398
452
  where new.from_state is not (select lifecycle from unit where id = new.unit_id)
399
453
  and exists (select 1 from unit_state where unit_id = new.unit_id);
454
+ -- A successor's state is read from its history, not its lifecycle column: a row written in the same statement may not be applied yet
455
+ select raise(abort, 'not a lifecycle change a unit can make: withdrawn is final, and a superseded unit only returns to candidate once every successor is withdrawn')
456
+ where exists (select 1 from unit_state where unit_id = new.unit_id) and not (
457
+ (new.from_state = 'candidate' and new.to_state in ('active', 'superseded', 'withdrawn'))
458
+ or (new.from_state = 'active' and new.to_state in ('candidate', 'superseded', 'withdrawn'))
459
+ or (new.from_state = 'superseded' and new.to_state = 'candidate' and not exists (
460
+ select 1 from unit_link l join unit s on s.id = l.from_unit where l.to_unit = new.unit_id and l.kind = 'supersedes'
461
+ and s.extraction = 'supported' and s.unsourced = 0
462
+ and (select to_state from unit_state where unit_id = l.from_unit order by id desc limit 1) is not 'withdrawn')));
400
463
  select raise(abort, 'a quarantined or unsourced unit cannot become active')
401
464
  where new.to_state = 'active' and exists (select 1 from unit where id = new.unit_id and (extraction <> 'supported' or unsourced = 1));
402
- select raise(abort, 'an active decision or constraint needs unretracted evidence and adoption')
403
- where new.to_state = 'active' and exists (select 1 from unit u where u.id = new.unit_id and u.kind in ('decision', 'constraint') and (
404
- 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)
405
- or not exists (select 1 from unit_adoption a where a.unit_id = u.id and a.retracted_at is null)));
406
- select raise(abort, 'an active implementation needs code or commit evidence')
407
- where new.to_state = 'active' and exists (select 1 from unit u where u.id = new.unit_id and u.kind = 'implementation' and not (
408
- 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
409
- and e.retracted_at is null and e.role = 'implements' and s.kind in ('commit_message', 'file_excerpt'))
410
- or exists (select 1 from unit_anchor a where a.unit_id = u.id and a.retired_at is null and a.role = 'evidence'
411
- and (a.commit_sha is not null or (a.edit_observation_id is not null and exists (select 1 from unit_evidence e
412
- join source s on s.id = e.source_id join edit_observation o on o.id = a.edit_observation_id
413
- where e.unit_id = u.id and e.option_id is null and e.retracted_at is null and e.role = 'implements'
414
- and s.session_id = o.session_id))))));
415
- select raise(abort, 'an active unit needs unretracted evidence')
416
- where new.to_state = 'active' and exists (select 1 from unit u where u.id = new.unit_id and u.kind in ('finding', 'dead_end', 'question')
417
- 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));
465
+ select raise(abort, (select missing from unit_support where unit_id = new.unit_id))
466
+ where new.to_state = 'active' and (select missing from unit_support where unit_id = new.unit_id) is not null;
418
467
  -- A reconsider condition is the owner's: each needs a quote of the owner, written when the unit is saved. A quote retracted later, or
419
468
  -- forgotten (forget's recheck is exempt, since the row is gone), leaves the unit as it was, and readers show the condition as unsupported
420
469
  select raise(abort, 'a reconsider condition needs a quote of the owner')
421
470
  where new.to_state = 'active' and new.forget_id is null and exists (select 1 from unit_option o where o.unit_id = new.unit_id
422
471
  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
423
472
  where e.option_id = o.id and e.role = 'reconsiders' and s.author_kind = 'owner'));
424
- select raise(abort, 'superseded needs a supersedes link from its successor')
425
- where new.to_state = 'superseded' and not exists (select 1 from unit_link where to_unit = new.unit_id and kind = 'supersedes');
473
+ select raise(abort, 'superseded needs a supersedes link from an active successor')
474
+ where new.to_state = 'superseded' and not exists (select 1 from unit_link l join unit s on s.id = l.from_unit
475
+ where l.to_unit = new.unit_id and l.kind = 'supersedes' and s.lifecycle = 'active');
426
476
  end;
427
477
  -- The one update allowed is the foreign key action clearing source_id after its source was forgotten
428
478
  create trigger unit_state_append_only before update on unit_state
@@ -437,17 +487,29 @@ end;
437
487
  create trigger unit_state_apply after insert on unit_state begin
438
488
  update unit set lifecycle = new.to_state, revision = revision + 1 where id = new.unit_id;
439
489
  end;
490
+ -- Withdrawing a record's last live successor brings the record back to candidate, to be judged again. Successors are read from history:
491
+ -- the withdrawal just written may not be applied to its unit yet
492
+ create trigger unit_state_restore after insert on unit_state when new.to_state = 'withdrawn' begin
493
+ insert into unit_state (unit_id, from_state, to_state, at, reason, source_id, run_id, forget_id)
494
+ select o.id, 'superseded', 'candidate', new.at, 'its successor was withdrawn', new.source_id, new.run_id, new.forget_id
495
+ from unit_link l join unit o on o.id = l.to_unit
496
+ where l.from_unit = new.unit_id and l.kind = 'supersedes' and o.lifecycle = 'superseded'
497
+ and not exists (select 1 from unit_link k join unit s on s.id = k.from_unit where k.to_unit = o.id and k.kind = 'supersedes'
498
+ and k.from_unit <> new.unit_id and s.extraction = 'supported' and s.unsourced = 0
499
+ and (select to_state from unit_state where unit_id = k.from_unit order by id desc limit 1) is not 'withdrawn');
500
+ end;
440
501
 
441
502
  -- Where a unit applies in code, or code cited as evidence. Validated against the working tree when served, never cached here.
442
503
  create table unit_anchor (
443
504
  id integer primary key autoincrement not null,
444
505
  unit_id integer not null references unit (id) on delete cascade,
445
- path text not null check (path <> '' and path not glob '/*' and path not glob '*[/]..[/]*' and path not glob '..[/]*'
446
- and path not glob '*[/]..' and path <> '..' and path not glob '*\*' and path not glob '[A-Za-z]:*'),
506
+ path text not null check (path <> '' and path <> '.' and path <> '..' and path not glob '/*' and path not glob '[A-Za-z]:*' and path not glob '*\*'
507
+ and path not glob '*//*' and path not glob './*' and path not glob '../*' and path not glob '*/./*' and path not glob '*/../*'
508
+ and path not glob '*/.' and path not glob '*/..' and path not glob '*[' || char(1) || '-' || char(31) || char(127) || '-' || char(159) || ']*'),
447
509
  symbol text,
448
510
  commit_sha text check (commit_sha is null or (length(commit_sha) = 40 and commit_sha not glob '*[^0-9a-f]*')),
449
511
  line_start integer check (line_start > 0),
450
- line_end integer check (line_end >= line_start),
512
+ line_end integer check (line_end is null or (line_start is not null and line_end >= line_start)),
451
513
  excerpt text,
452
514
  role text not null check (role in ('applies_to', 'evidence')),
453
515
  -- For work recorded before a commit: the edit observation of this path in the session, checked against the working tree when saved
@@ -459,6 +521,13 @@ create table unit_anchor (
459
521
  ) strict;
460
522
  create index unit_anchor_path on unit_anchor (path, role) where retired_at is null;
461
523
  create index unit_anchor_unit on unit_anchor (unit_id, retired_at);
524
+ -- One live anchor per place of a unit: a symbol, or the lines when it has no symbol
525
+ create unique index unit_anchor_live_once on unit_anchor (unit_id, path, role, coalesce(commit_sha, ''), coalesce(symbol, ''),
526
+ coalesce(case when symbol is null then line_start end, 0), coalesce(case when symbol is null then line_end end, 0))
527
+ where retired_at is null;
528
+ create index unit_anchor_observation on unit_anchor (edit_observation_id) where edit_observation_id is not null;
529
+ create index unit_anchor_replaced on unit_anchor (replaced_by) where replaced_by is not null;
530
+ create index unit_anchor_run on unit_anchor (run_id);
462
531
  -- Anchors are retired and replaced, never edited in place (except setting retired_at and replaced_by once)
463
532
  create trigger unit_anchor_frozen before update on unit_anchor begin
464
533
  select raise(abort, 'anchors are replaced, not edited; retirement happens once')
@@ -466,11 +535,38 @@ create trigger unit_anchor_frozen before update on unit_anchor begin
466
535
  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
467
536
  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
468
537
  or old.retired_at is not null or new.retired_at is null;
538
+ select raise(abort, 'an anchor is replaced by another anchor of the same record')
539
+ where new.replaced_by is not null and (new.replaced_by = new.id
540
+ or not exists (select 1 from unit_anchor where id = new.replaced_by and unit_id = new.unit_id));
469
541
  end;
470
542
  create trigger unit_anchor_no_delete before delete on unit_anchor when exists (select 1 from unit where id = old.unit_id) begin
471
543
  select raise(abort, 'anchors are retired, never deleted');
472
544
  end;
473
545
 
546
+ -- What an active unit must have, in one place: missing says what it lacks, and is null when it lacks nothing. Activating reads it, and so
547
+ -- does every change that can take support away (a retraction, retiring an anchor), so the two never disagree.
548
+ -- Evidence on an option supports the option, never the unit.
549
+ create view unit_support as
550
+ select u.id as unit_id, case
551
+ when u.kind in ('decision', 'constraint') and (
552
+ 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)
553
+ or not exists (select 1 from unit_adoption a where a.unit_id = u.id and a.retracted_at is null))
554
+ then 'an active decision or constraint needs unretracted evidence and adoption'
555
+ when u.kind = 'implementation' and not (
556
+ 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
557
+ and e.retracted_at is null and e.role = 'implements' and s.kind in ('commit_message', 'file_excerpt'))
558
+ or exists (select 1 from unit_anchor a where a.unit_id = u.id and a.retired_at is null and a.role = 'evidence'
559
+ and (a.commit_sha is not null or (a.edit_observation_id is not null and exists (select 1 from unit_evidence e
560
+ join source s on s.id = e.source_id join edit_observation o on o.id = a.edit_observation_id
561
+ where e.unit_id = u.id and e.option_id is null and e.retracted_at is null and e.role = 'implements'
562
+ and s.session_id = o.session_id)))))
563
+ then 'an active implementation needs code or commit evidence'
564
+ when u.kind in ('finding', 'dead_end', 'question')
565
+ 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)
566
+ then 'an active unit needs unretracted evidence'
567
+ end as missing
568
+ from unit u;
569
+
474
570
  -- Search-only aliases in Japanese and English, written by the agent with the unit. Each set is bound to the unit's content_hash when written;
475
571
  -- only the newest set whose hash matches is indexed. Older sets stay for as-of snapshots. Never evidence, never shown as something said.
476
572
  create table unit_alias (
@@ -482,7 +578,10 @@ create table unit_alias (
482
578
  added_at text not null check (strftime('%Y-%m-%dT%H:%M:%fZ', added_at) is added_at)
483
579
  ) strict;
484
580
  create index unit_alias_unit on unit_alias (unit_id, id);
581
+ create index unit_alias_run on unit_alias (run_id);
485
582
  create trigger unit_alias_terms before insert on unit_alias begin
583
+ select raise(abort, 'an alias set is bound to the words of its unit as they are')
584
+ where new.content_hash is not (select content_hash from unit where id = new.unit_id);
486
585
  select raise(abort, 'each alias is a non-empty string of at most 40 characters')
487
586
  where exists (select 1 from json_each(new.terms) where type <> 'text' or length(trim(value)) = 0 or length(value) > 40);
488
587
  end;
@@ -517,6 +616,7 @@ create table field_def (
517
616
  check ((type = 'enum') = (enum_values is not null))
518
617
  ) strict;
519
618
  create index field_def_source on field_def (source_id);
619
+ create index field_def_run on field_def (run_id);
520
620
  create trigger field_def_check before insert on field_def begin
521
621
  select raise(abort, 'a field definition, its source, and its run belong to one project')
522
622
  where new.project_id is not (select project_id from source where id = new.source_id)
@@ -527,6 +627,9 @@ create trigger field_def_check before insert on field_def begin
527
627
  where not exists (select 1 from source where id = new.source_id and author_kind = 'owner');
528
628
  select raise(abort, 'field definition span is outside the source text')
529
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';
530
633
  select raise(abort, 'enum values are distinct non-empty strings of at most 100 characters')
531
634
  where new.enum_values is not null and (exists (select 1 from json_each(new.enum_values)
532
635
  where type <> 'text' or length(trim(value)) = 0 or length(value) > 100)
@@ -560,6 +663,7 @@ create table unit_field (
560
663
  ) strict;
561
664
  create index unit_field_def on unit_field (field_def_id);
562
665
  create index unit_field_source on unit_field (source_id);
666
+ create index unit_field_run on unit_field (run_id);
563
667
  create trigger unit_field_check before insert on unit_field begin
564
668
  select raise(abort, 'a field value, its unit, definition, source, and run belong to one project')
565
669
  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)
@@ -571,6 +675,9 @@ create trigger unit_field_check before insert on unit_field begin
571
675
  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);
572
676
  select raise(abort, 'field value span is outside the source text')
573
677
  where new.span_end > (select length(cast(text as blob)) from source where id = new.source_id);
678
+ select raise(abort, 'a span starts or ends inside a character')
679
+ where hex(substr((select cast(text as blob) from source where id = new.source_id), new.span_start + 1, 1)) between '80' and 'BF'
680
+ or hex(substr((select cast(text as blob) from source where id = new.source_id), new.span_end + 1, 1)) between '80' and 'BF';
574
681
  select raise(abort, 'the field does not apply to this kind of unit')
575
682
  where exists (select 1 from field_def d where d.id = new.field_def_id and json_array_length(d.kinds) > 0
576
683
  and not exists (select 1 from json_each(d.kinds) j where j.value = (select kind from unit where id = new.unit_id)));
@@ -600,6 +707,12 @@ create trigger unit_evidence_check before insert on unit_evidence begin
600
707
  or (select project_id from unit where id = new.unit_id) is not (select project_id from extraction_run where id = new.run_id);
601
708
  select raise(abort, 'evidence span is outside the source text')
602
709
  where new.span_end > (select length(cast(text as blob)) from source where id = new.source_id);
710
+ -- A retraction cites the owner's words, which only the retraction's own update checks: a row is never written already retracted
711
+ select raise(abort, 'evidence is written live, then retracted')
712
+ where new.retracted_at is not null or new.retraction_source_id is not null;
713
+ select raise(abort, 'a span starts or ends inside a character')
714
+ where hex(substr((select cast(text as blob) from source where id = new.source_id), new.span_start + 1, 1)) between '80' and 'BF'
715
+ 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
716
  select raise(abort, 'a reported speaker is the owner reporting someone else, so it must cite an owner session message')
604
717
  where new.reported_speaker is not null and (trim(new.reported_speaker) = '' or not exists (select 1 from source
605
718
  where id = new.source_id and kind = 'session_message' and author_kind = 'owner'));
@@ -618,36 +731,49 @@ create trigger unit_evidence_retract before update on unit_evidence begin
618
731
  where not exists (select 1 from source s where s.id = new.retraction_source_id and s.author_kind = 'owner'
619
732
  and s.project_id = (select project_id from unit where id = new.unit_id)
620
733
  and new.retraction_span_end <= length(cast(s.text as blob)));
734
+ select raise(abort, 'a span starts or ends inside a character')
735
+ 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'
736
+ 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';
621
737
  end;
622
738
  -- Deleting a project (or its unit or source) cascades; only a direct delete of a live link is refused.
623
- -- A retracted row whose retraction reason cites a forgotten source is removed with that source (the reason cannot outlive it).
739
+ -- A retracted row goes with the source its retraction reason cites (the reason cannot outlive it).
624
740
  create trigger unit_evidence_no_delete before delete on unit_evidence
625
741
  when exists (select 1 from unit where id = old.unit_id) and exists (select 1 from source where id = old.source_id)
626
- and not (old.retracted_at is not null and exists (select 1 from source_forgotten where source_id = old.retraction_source_id)) begin
742
+ and not (old.retracted_at is not null and not exists (select 1 from source where id = old.retraction_source_id)) begin
627
743
  select raise(abort, 'evidence is retracted, never deleted');
628
744
  end;
629
745
  create trigger unit_adoption_no_delete before delete on unit_adoption
630
746
  when exists (select 1 from unit where id = old.unit_id) and exists (select 1 from source where id = old.source_id)
631
- and not (old.retracted_at is not null and exists (select 1 from source_forgotten where source_id = old.retraction_source_id)) begin
747
+ and not (old.retracted_at is not null and not exists (select 1 from source where id = old.retraction_source_id)) begin
632
748
  select raise(abort, 'adoption is retracted, never deleted');
633
749
  end;
634
- -- A retraction that would leave an active unit without its required support must first move it back to candidate
750
+ -- A retraction, or retiring an anchor, that would leave an active unit without its required support must first move it back to candidate
635
751
  create trigger unit_evidence_retract_support after update of retracted_at on unit_evidence
636
- when exists (select 1 from unit where id = new.unit_id and lifecycle = 'active')
637
- and not exists (select 1 from unit_evidence where unit_id = new.unit_id and retracted_at is null) begin
752
+ when exists (select 1 from unit u join unit_support s on s.unit_id = u.id
753
+ where u.id = new.unit_id and u.lifecycle = 'active' and s.missing is not null) begin
638
754
  select raise(abort, 'move the unit back to candidate before retracting its last evidence');
639
755
  end;
640
756
  create trigger unit_adoption_retract_support after update of retracted_at on unit_adoption
641
- when exists (select 1 from unit where id = new.unit_id and lifecycle = 'active')
642
- and not exists (select 1 from unit_adoption where unit_id = new.unit_id and retracted_at is null) begin
757
+ when exists (select 1 from unit u join unit_support s on s.unit_id = u.id
758
+ where u.id = new.unit_id and u.lifecycle = 'active' and s.missing is not null) begin
643
759
  select raise(abort, 'move the unit back to candidate before retracting its last adoption');
644
760
  end;
761
+ create trigger unit_anchor_retire_support after update of retired_at on unit_anchor
762
+ when exists (select 1 from unit u join unit_support s on s.unit_id = u.id
763
+ where u.id = new.unit_id and u.lifecycle = 'active' and s.missing is not null) begin
764
+ select raise(abort, 'move the unit back to candidate before retiring its last code anchor');
765
+ end;
645
766
  create trigger unit_adoption_check before insert on unit_adoption begin
646
767
  select raise(abort, 'adoption and unit belong to different projects')
647
768
  where (select project_id from unit where id = new.unit_id) is not (select project_id from source where id = new.source_id)
648
769
  or (select project_id from unit where id = new.unit_id) is not (select project_id from extraction_run where id = new.run_id);
649
770
  select raise(abort, 'adoption span is outside the source text')
650
771
  where new.span_end > (select length(cast(text as blob)) from source where id = new.source_id);
772
+ select raise(abort, 'adoption is written live, then retracted')
773
+ where new.retracted_at is not null or new.retraction_source_id is not null;
774
+ select raise(abort, 'a span starts or ends inside a character')
775
+ where hex(substr((select cast(text as blob) from source where id = new.source_id), new.span_start + 1, 1)) between '80' and 'BF'
776
+ or hex(substr((select cast(text as blob) from source where id = new.source_id), new.span_end + 1, 1)) between '80' and 'BF';
651
777
  end;
652
778
  create trigger unit_adoption_retract before update on unit_adoption begin
653
779
  select raise(abort, 'adoption is only ever retracted, once')
@@ -658,13 +784,29 @@ create trigger unit_adoption_retract before update on unit_adoption begin
658
784
  where not exists (select 1 from source s where s.id = new.retraction_source_id and s.author_kind = 'owner'
659
785
  and s.project_id = (select project_id from unit where id = new.unit_id)
660
786
  and new.retraction_span_end <= length(cast(s.text as blob)));
787
+ select raise(abort, 'a span starts or ends inside a character')
788
+ 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'
789
+ 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';
661
790
  end;
662
791
  create trigger unit_link_check before insert on unit_link begin
663
792
  select raise(abort, 'linked units belong to different projects')
664
793
  where (select project_id from unit where id = new.from_unit) is not (select project_id from unit where id = new.to_unit)
665
794
  or (select project_id from unit where id = new.from_unit) is not (select project_id from extraction_run where id = new.run_id);
795
+ select raise(abort, 'a record supersedes one of its own kind; a decision and a constraint can replace each other')
796
+ 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
797
+ and (a.kind = b.kind or (a.kind in ('decision', 'constraint') and b.kind in ('decision', 'constraint'))));
798
+ -- One live successor at a time. A withdrawn one gives its place up, and a quarantined or unsourced one never takes it: it can never
799
+ -- become active, nor be withdrawn. States are read from history, as in unit_state_rules
800
+ select raise(abort, 'the record already has a successor that is not withdrawn')
801
+ where new.kind = 'supersedes'
802
+ and exists (select 1 from unit n where n.id = new.from_unit and n.extraction = 'supported' and n.unsourced = 0)
803
+ and exists (select 1 from unit_link l join unit s on s.id = l.from_unit
804
+ where l.to_unit = new.to_unit and l.kind = 'supersedes' and s.extraction = 'supported' and s.unsourced = 0
805
+ and (select to_state from unit_state where unit_id = l.from_unit order by id desc limit 1) is not 'withdrawn');
666
806
  end;
667
807
  create trigger unit_state_project before insert on unit_state begin
808
+ select raise(abort, 'a state comes after its unit was created')
809
+ where new.at < (select created_at from unit where id = new.unit_id);
668
810
  select raise(abort, 'state and unit belong to different projects')
669
811
  where (new.run_id is not null
670
812
  and (select project_id from unit where id = new.unit_id) is not (select project_id from extraction_run where id = new.run_id))
@@ -674,6 +816,8 @@ create trigger unit_state_project before insert on unit_state begin
674
816
  and (select project_id from unit where id = new.unit_id) is not (select project_id from source where id = new.source_id));
675
817
  end;
676
818
  create trigger unit_anchor_project before insert on unit_anchor begin
819
+ select raise(abort, 'an anchor is replaced by another anchor of the same record')
820
+ 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);
677
821
  select raise(abort, 'anchor and unit belong to different projects')
678
822
  where (select project_id from unit where id = new.unit_id) is not (select project_id from extraction_run where id = new.run_id);
679
823
  select raise(abort, 'the edit observation must be of this path in a session of the same project')
@@ -688,11 +832,6 @@ create trigger source_processing_project before insert on source_processing begi
688
832
  select raise(abort, 'source and run belong to different projects')
689
833
  where (select project_id from source where id = new.source_id) is not (select project_id from extraction_run where id = new.run_id);
690
834
  end;
691
- create trigger external_reference_check before insert on external_reference begin
692
- select raise(abort, 'an external reference needs an owner span of the same project')
693
- where not exists (select 1 from source s where s.id = new.owner_source_id and s.author_kind = 'owner' and s.project_id = new.project_id
694
- and new.span_end <= length(cast(s.text as blob)));
695
- end;
696
835
 
697
836
  -- Every change to a unit's relations raises its revision (stale drafts are refused against it)
698
837
  create trigger unit_rev_evidence_i after insert on unit_evidence begin update unit set revision = revision + 1 where id = new.unit_id; end;
@@ -793,6 +932,7 @@ create table work (
793
932
  unique (project_id, key)
794
933
  ) strict;
795
934
  create index work_open on work (project_id, updated_at desc) where status in ('active', 'blocked', 'paused');
935
+ create index work_run on work (run_id) where run_id is not null;
796
936
 
797
937
  -- What a delivery hook emitted or suppressed, and how many eligible units it left out. No source text is copied here.
798
938
  -- chars is the delivered length without the omission note (Sphica's own text), since the read budget adds it up.
@@ -814,6 +954,24 @@ create table delivery_unit (
814
954
  unit_id integer not null references unit (id) on delete cascade,
815
955
  primary key (delivery_id, unit_id)
816
956
  ) strict;
957
+ create index delivery_unit_unit on delivery_unit (unit_id);
958
+
959
+ -- The one way the record server writes a source. It has no session columns, so it can never write a session message: those are the
960
+ -- owner's own words, and only capture writes them. A view insert returns no id; the writer looks the row up by its item key
961
+ create view ingest_source as
962
+ select project_id, kind, artifact, external_id, revision, author_kind, author_login, author_external_id, author_association,
963
+ parent_external_id, event_kind, url, created_at, available_at, captured_at, text, truncated, redacted, original_bytes,
964
+ content_hash, path, line_start, line_end, diff_hunk, commit_sha, blob_sha, indexed
965
+ from source where session_id is null;
966
+ create trigger ingest_source_insert instead of insert on ingest_source begin
967
+ insert into source (project_id, kind, artifact, external_id, revision, author_kind, author_login, author_external_id, author_association,
968
+ parent_external_id, event_kind, url, created_at, available_at, captured_at, text, truncated, redacted, original_bytes,
969
+ content_hash, path, line_start, line_end, diff_hunk, commit_sha, blob_sha, indexed, session_id, turn_id)
970
+ values (new.project_id, new.kind, new.artifact, new.external_id, new.revision, new.author_kind, new.author_login, new.author_external_id,
971
+ new.author_association, new.parent_external_id, new.event_kind, new.url, new.created_at, new.available_at, new.captured_at,
972
+ new.text, coalesce(new.truncated, 0), coalesce(new.redacted, 0), new.original_bytes, new.content_hash, new.path, new.line_start,
973
+ new.line_end, new.diff_hunk, new.commit_sha, new.blob_sha, new.indexed, null, null);
974
+ end;
817
975
 
818
976
  -- The views the capture connection may write. The capture authorizer allows inserts into these views only; the triggers derive
819
977
  -- project, artifact, and indexing from the session and the speaker, so capture cannot write another project's rows or third-party text.
@@ -867,4 +1025,4 @@ create trigger capture_delivery_insert instead of insert on capture_delivery beg
867
1025
  select last_insert_rowid(), j.value from json_each(coalesce(new.units, '[]')) j where true on conflict do nothing;
868
1026
  end;
869
1027
 
870
- pragma user_version = 4;
1028
+ pragma user_version = 5;