A checklist is something a note has, not something a note is
CI & Build / Build now, or wait for Android? (push) Successful in 2s
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 7s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Tauri desktop (Linux) (push) Failing after 9s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Failing after 8s
Desktop (Tauri) / Update manifest (push) Skipped
CI & Build / Python tests (push) Successful in 13s
Android / Kotlin + Rust (APK) (push) Failing after 1m43s

`kind` was never a type. A plain TEXT column with no enum and no CHECK behind
it, compared against a hardcoded ("text", "list") tuple in six places;
`note_items` was always an ordinary child table keyed by note_id; serialization
already emitted `items` whatever the kind; and the Android editor already
toggled between the two losslessly, saying so in a comment. The storage has
modelled "a body plus optional checkable items" the whole time. This deletes the
gates that forbade it.

Every surface: the create/PATCH gates, the ?kind= filter and its saved-filter
facet, the three import/export branches, the column (alembic 0025); the core's
`kind` field, its SQLite column (user_version 6), the sync wire, push and pull;
the FFI records and `NoteEdit::Kind`; and on Android `NoteKind.kt`, `DraftKind`,
the compose sheet's Note/List switch, and the branches in the card, the editor
and the chrome.

The editor's note⇄list toggle becomes "Add a checklist" — on both the web and
Android. It is not a conversion any more: nothing moves, nothing is swapped, the
body stays exactly where it is and the note gains somewhere to put items. The
card renders both, in order.

Two things that fell out of the merge rather than being aimed at:

- The Keep importer was DISCARDING `textContent` whenever a note also had
  `listContent`, because the target could only hold one. Both survive now, and
  the test says so.
- Markdown export wrote the body OR the checklist. It writes both.

Protocol goes to v2, floor included: dropping a field a v1 client sends and
expects back is breaking. `title` leaves in step 3 and lands in the same
generation, so it needs no further bump. This is the change that will make the
0.1.227 build on the operator's phone refuse to sync — the in-app updater is
independent of the handshake and remains the recovery path.

The V1 SQLite schema deliberately KEEPS the kind column. V1 is the historical
schema and every later block alters it, so removing it there would make a fresh
database run V1 without the column and then v6's DROP COLUMN against a column
that never existed — "no such column: kind" on every new install.
This commit is contained in:
2026-08-22 12:53:53 -04:00
parent 229076c82d
commit c46a4a7709
34 changed files with 240 additions and 323 deletions
-2
View File
@@ -573,7 +573,6 @@ mod tests {
title: title.to_string(),
body: body.to_string(),
color: "default".to_string(),
kind: None,
items: None,
}
}
@@ -677,7 +676,6 @@ mod tests {
title: "Packing".to_string(),
body: String::new(),
color: "default".to_string(),
kind: Some("list".to_string()),
items: Some(vec!["socks".to_string()]),
})
.expect("create");
+2 -12
View File
@@ -35,7 +35,6 @@ pub struct Note {
pub display_title: String,
pub body: String,
pub color: String,
pub kind: String,
pub position: i64,
pub pinned: bool,
pub archived: bool,
@@ -134,7 +133,6 @@ impl From<core_models::Note> for Note {
display_title,
body,
color,
kind,
position,
pinned,
archived,
@@ -155,7 +153,6 @@ impl From<core_models::Note> for Note {
display_title,
body,
color,
kind,
position,
pinned,
archived,
@@ -294,7 +291,6 @@ pub struct NoteQuery {
pub struct NoteFacets {
pub q: Option<String>,
pub color: Option<String>,
pub kind: Option<String>,
pub label: Option<Vec<String>>,
pub has_reminder: Option<bool>,
pub has_attachment: Option<bool>,
@@ -324,7 +320,6 @@ impl From<NoteFacets> for core_models::Facets {
let NoteFacets {
q,
color,
kind,
label,
has_reminder,
has_attachment,
@@ -334,7 +329,6 @@ impl From<NoteFacets> for core_models::Facets {
core_models::Facets {
q,
color,
kind,
label,
has_reminder,
has_attachment,
@@ -351,8 +345,8 @@ pub struct NoteDraft {
pub body: String,
/// "default" unless the user picked a colour.
pub color: String,
pub kind: Option<String>,
/// Checklist lines, for `kind = "checklist"`.
/// Checklist lines. A note can carry both a body and items (M13 step 2), so this
/// is not an alternative to `body` — it is an addition to it.
pub items: Option<Vec<String>>,
}
@@ -362,14 +356,12 @@ impl From<NoteDraft> for core_models::NoteCreateInput {
title,
body,
color,
kind,
items,
} = value;
core_models::NoteCreateInput {
title,
body,
color,
kind,
items,
}
}
@@ -389,7 +381,6 @@ pub enum NoteEdit {
ClearTitle,
Body { value: String },
Color { value: String },
Kind { value: String },
Pinned { value: bool },
Archived { value: bool },
RemindAt { value: String },
@@ -412,7 +403,6 @@ impl NoteEdit {
NoteEdit::ClearTitle => ("title", Value::Null),
NoteEdit::Body { value } => ("body", Value::String(value)),
NoteEdit::Color { value } => ("color", Value::String(value)),
NoteEdit::Kind { value } => ("kind", Value::String(value)),
NoteEdit::Pinned { value } => ("pinned", Value::Bool(value)),
NoteEdit::Archived { value } => ("archived", Value::Bool(value)),
NoteEdit::RemindAt { value } => ("remind_at", Value::String(value)),