task_snapshots gains via and read_at, an integer-bucket CHECK with a tombstone exception, the read-ordering rule in task_external_changes and the tasks_open view. task.missing names its task and a reason. Prototype rerun on Node 24.21.0 and 26.8.1; a mutant without the read_at rule reports the broker's own move as external. Lead decision 52 (B3). Co-Authored-By: Claude Opus 5.5 <[email protected]>
3.8 KiB
Slice 1 prototype, v3 (addendum B section 5, lead decision 52)
Darkwing, 2026-10-04, for the slice 1 brief (docs/plans/2026-10-04_slice-1.md,
row S2 starts from this schema). The v1 and v2 files are unchanged.
schema-v3.sql is a full schema that stands on its own and replaces v2.
It isn't a migration.
What changed from schema-v2.sql. diff schema-v2.sql schema-v3.sql
shows all of it:
task_snapshots.via: which read produced a poll snapshot, one ofboard(the open-task kanban listing),cursor(theupdatedcursor),task(a singleGETafter a task left the open set) orreconcile.task_snapshots.read_at: when the broker sent that read. A poll row needs bothviaandread_at, and aselfrow has neither. For aselfrow,atis when the write's response arrived.- Every snapshot carries an integer
fields.bucket. The one exception is a tombstone, a poll row withviataskand a textfields.gone. The CHECK usesIS, not=: a missing JSON path makesjson_typeNULL, and SQLite passes a CHECK that evaluates to NULL. Addendum B section 5 records how I found that. task_external_changesaddsp.read_at > ls.at. A move between columns that aren't done doesn't changeupdated(probe P3), soupdatedalone can't order a board read against the broker's own move. The view also returnsvia.- New view
tasks_open: tasks whose latest snapshot is open and not a tombstone. The poll compares the board read with it. - New trigger
events_task_missing_body. Atask.missingevent names its task insubjectand carriesreasonmoved,not-foundorno-access;movedalso carries the newprojectas an integer. Addendum B section 5 gave the body. The trigger enforces it, the same way v2 enforces thecredential.*body. It is the one addition beyond section 5's text.
proto-v3.mjs is proto-v2.mjs with two sections changed. The
task.missing cases now include four refusals, and the task_snapshots
section was rewritten for the new columns. Apart from the header comment,
the temp directory prefix and the schema file name, the rest is
unchanged.
Results: proto-v3-node24.txt (Node 24.21.0 in the node:24 image, no
network, the directory mounted read-only) and proto-v3-node26.txt
(Node 26.8.1 on the host). Both use SQLite 3.53.4, and the outputs differ
only in the version line. Every refusal the script expects happens:
- a poll row without
viaorread_at, aselfrow withvia, an unknownvia; - a snapshot with no bucket or a text bucket, a
goneon a board read or on aselfrow; - a
task.missingwith no reason, an unknown reason, no subject, ormovedwithout a project.
The views, in order:
- an unchanged cursor read isn't external;
- a person's edit in the same second as a write is external,
viacursor; - a board read sent before the broker's move finished isn't external;
- a stale cursor read isn't external;
- a board read that agrees with the move isn't external;
- a person's move with
updatedunchanged is external,viaboard; - a tombstone drops the task from
tasks_open.
UPDATE, DELETE and INSERT OR REPLACE are refused on all eight tables.
Dropping one guard and reopening gives MISMATCH.
Mutant: the same script against schema-v3.sql without
AND p.read_at > ls.at reports the stale board read as an external
change ([{"task_ref":"vikunja:3/41"}]). Run in
~/darkwing-scratch/v3mut, output in mutant.txt there.
Limits, the same as v1 and v2. The triggers catch our own bugs. A process running as the same user can still drop a trigger, and the digest check only notices that afterwards. The views are demonstrations. The broker runs its own queries, and the views show the rules in SQL. The digest's field list and the rule to write a poll row only when the digest changed are broker behaviour (addendum B section 5), so the schema can't check them.