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]>
76 lines
3.8 KiB
Markdown
76 lines
3.8 KiB
Markdown
# 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 of
|
|
`board` (the open-task kanban listing), `cursor` (the `updated`
|
|
cursor), `task` (a single `GET` after a task left the open set) or
|
|
`reconcile`.
|
|
- `task_snapshots.read_at`: when the broker sent that read. A poll row
|
|
needs both `via` and `read_at`, and a `self` row has neither. For a
|
|
`self` row, `at` is when the write's response arrived.
|
|
- Every snapshot carries an integer `fields.bucket`. The one exception is
|
|
a tombstone, a poll row with `via` `task` and a text `fields.gone`. The
|
|
CHECK uses `IS`, not `=`: a missing JSON path makes `json_type` NULL,
|
|
and SQLite passes a CHECK that evaluates to NULL. Addendum B section 5
|
|
records how I found that.
|
|
- `task_external_changes` adds `p.read_at > ls.at`. A move between
|
|
columns that aren't done doesn't change `updated` (probe P3), so
|
|
`updated` alone can't order a board read against the broker's own move.
|
|
The view also returns `via`.
|
|
- 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`. A `task.missing` event names its
|
|
task in `subject` and carries `reason` `moved`, `not-found` or
|
|
`no-access`; `moved` also carries the new `project` as an integer.
|
|
Addendum B section 5 gave the body. The trigger enforces it, the same
|
|
way v2 enforces the `credential.*` 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 `via` or `read_at`, a `self` row with `via`, an
|
|
unknown `via`;
|
|
- a snapshot with no bucket or a text bucket, a `gone` on a board read or
|
|
on a `self` row;
|
|
- a `task.missing` with no reason, an unknown reason, no subject, or
|
|
`moved` without 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, `via` `cursor`;
|
|
- 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 `updated` unchanged is external, `via` `board`;
|
|
- 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.
|