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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/db/migrations/0005.check.sql +36 -0
- package/db/migrations/0005.sql +1172 -0
- package/db/schema.sql +230 -72
- package/dist/capture.js +81 -13
- package/dist/cli.js +179 -29
- package/dist/deliver.js +81 -13
- package/dist/mcp-record.js +184 -52
- package/dist/mcp.js +2 -4
- package/package.json +1 -1
- package/skills/glean/SKILL.md +1 -1
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
|
|
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
|
|
76
|
+
-- The comment or thread this replies to
|
|
69
77
|
parent_external_id text,
|
|
70
|
-
-- For pr_event: merged
|
|
71
|
-
event_kind text check (event_kind in ('merged'
|
|
72
|
-
|
|
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 '
|
|
83
|
-
and path not glob '
|
|
84
|
-
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 '
|
|
179
|
-
and path not glob '
|
|
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'
|
|
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 (
|
|
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'
|
|
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
|
-
|
|
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
|
|
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', '
|
|
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,
|
|
403
|
-
where new.to_state = 'active' 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
|
|
425
|
-
where new.to_state = 'superseded' and not exists (select 1 from unit_link
|
|
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 '
|
|
446
|
-
and path not glob '
|
|
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
|
|
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
|
|
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
|
|
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
|
|
637
|
-
|
|
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
|
|
642
|
-
|
|
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 =
|
|
1028
|
+
pragma user_version = 5;
|