Files
inkwell/alembic/versions/0027_checklist_items_into_body.py
T
bvandeusenandClaude Opus 5.5 a51a563fe7
CI & Build / Python lint (push) Successful in 2s
CI & Build / Build now, or wait for Android? (push) Successful in 2s
Android / Build, or is the channel already serving this? (push) Successful in 3s
Android / Core and FFI clippy and tests (push) Skipped
Android / Kotlin + Rust (APK) (push) Skipped
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
CI & Build / Web typecheck and unit tests (push) Successful in 9s
CI & Build / Python tests (push) Successful in 13s
Android / Build the server image (push) Skipped
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Skipped
Desktop (Tauri) / Tauri desktop (Linux) (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Skipped
Desktop (Tauri) / Update manifest (push) Skipped
CI & Build / integration (push) Successful in 1m30s
CI & Build / Build & push image (push) Successful in 32s
Server comments describe the code to a stranger (#6221)
Rule 32 "Comments describe the product to a stranger" sweep over
src/, alembic/ and tests/. Comments and docstrings now say what the code
does and the reason as a present-tense constraint, briefly.

Removed:
- issue, task, note, rule, milestone and family-idea references
- "used to" / before-and-after narration
- quotes of decisions and incident stories from the deploying instance
- paragraph-length arguments, where a line or two carries the fact

Kept every hazard as a present-tense fact, e.g. timing oracles, the
proxy hop direction, and migrations that must stay frozen.

Also:
- Three test names lose "still"/"used to"/"pre_m304" phrasing.
- pyproject's description drops "FabledSword family".

The history these comments carried stays in Scribe and in git log.
A comment-stripping diff (Scribe snippet #6226 "codeonly.py — prove a
diff changed only comments") shows no code changed apart from the
renamed tests and the description string.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 20:07:47 -04:00

94 lines
3.5 KiB
Python

"""fold note_items into the note body and drop the table
Revision ID: 0027
Revises: 0026
Create Date: 2026-08-24
A checklist item becomes a `- [ ] milk` line of `notes.body`, so a list can sit
between two paragraphs, and `note_items` goes.
## This migration rewrites note bodies
* Rows are read before the table is dropped, in this one transaction.
* The existing body is only appended to.
* The layout (a blank line between prose and the list, nothing between items) is
what the export writes and what `derive::append_item` produces, so the clients
migrate their own stores independently and still agree with the server.
The fold is inlined rather than imported from `notes/checklist.py`: a migration must
keep producing what it produced the day it ran.
## `updated_at` is left alone
Raw SQL, so `onupdate` never fires, and a client holding an unpushed edit keeps the
newer `updated_at` and wins last-write-wins. The `sync_revision` trigger does fire,
so every migrated note is pulled once and clients converge.
## The downgrade is not a true inverse
It recreates an empty `note_items` and leaves the bodies alone: nothing tells a line
this wrote from one somebody typed. The real rollback is a database restore. The
table must exist because 0015's downgrade drops a trigger on it.
"""
import re
import sqlalchemy as sa
from alembic import op
from sqlalchemy.dialects.postgresql import UUID
revision = "0027"
down_revision = "0026"
branch_labels = None
depends_on = None
_TASK_RE = re.compile(r"^\s*[-*] +\[[ xX]\](?: +.*)?$")
def _append_item(body: str, text: str, checked: bool) -> str:
mark = "x" if checked else " "
text = (text or "").strip()
line = f"- [{mark}] {text}" if text else f"- [{mark}]"
trimmed = (body or "").rstrip("\n")
if not trimmed.strip():
return line
follows_a_list = bool(_TASK_RE.match(trimmed.split("\n")[-1]))
return f"{trimmed}\n{line}" if follows_a_list else f"{trimmed}\n\n{line}"
def upgrade():
bind = op.get_bind()
rows = bind.execute(
sa.text("SELECT note_id, text, checked FROM note_items ORDER BY note_id, position, created_at")
).fetchall()
grouped: dict = {}
for note_id, text, checked in rows:
grouped.setdefault(note_id, []).append((text, bool(checked)))
for note_id, items in grouped.items():
body = bind.execute(sa.text("SELECT body FROM notes WHERE id = :id"), {"id": note_id}).scalar()
# An item whose note is gone has nothing to fold into. The foreign key should
# prevent it; skipping avoids a half-migrated database.
if body is None:
continue
for text, checked in items:
body = _append_item(body, text, checked)
bind.execute(sa.text("UPDATE notes SET body = :body WHERE id = :id"), {"body": body, "id": note_id})
op.drop_table("note_items")
def downgrade():
# Exactly as 0006 created it, index name included: 0015's downgrade names both
# the table and its trigger.
op.create_table(
"note_items",
sa.Column("id", UUID(as_uuid=True), primary_key=True),
sa.Column("note_id", UUID(as_uuid=True), sa.ForeignKey("notes.id", ondelete="CASCADE"), nullable=False),
sa.Column("text", sa.Text(), nullable=False),
sa.Column("checked", sa.Boolean(), nullable=False, server_default=sa.false()),
sa.Column("position", sa.Integer(), nullable=False, server_default="0"),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()),
)
op.create_index("ix_note_items_note", "note_items", ["note_id"])