copy: the product says "tags" now, and the schema keeps saying Label
Android / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / Build now, or wait for Android? (push) Successful in 3s
CI & Build / Python lint (push) Successful in 3s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 7s
CI & Build / Python tests (push) Successful in 16s
CI & Build / integration (push) Successful in 23s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 3m7s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 5m17s
Desktop (Tauri) / Update manifest (push) Successful in 4s
Android / Kotlin + Rust (APK) (push) Successful in 8m4s

Two words for one concept cost real comprehension: over a single exchange
the operator concluded that auto-tagging did not exist (it does, in
`derive.rs`) and that a tag-management view did not exist (it does,
`LabelsModal.vue`). The `#` is how most of these get made, so the `#` wins
the noun.

User-visible strings only, on all three surfaces plus the server's errors.
`Label`, `NoteLabel`, `via_tag`, `label_id`, the tables, `/api/labels` and
the FFI names are all untouched — renaming those touches migrations and the
wire format to buy nothing a reader can see.

Two of these were more than a find-and-replace:

  * Android's `label_from_tag` said "from #tag", sitting beside a chip that
    already renders as `#name`. Once every one of them IS a tag that hint is
    circular. What it actually tells you is that the note's BODY owns this
    one — which is why it alone has no remove cross — so it now says "from
    the text".

  * The web's empty state said "No labels yet — create one above" while
    Android's already mentioned the `#` route. The web now says it too. That
    is the exact fact the operator did not have.

The paired `aria-label`s went with their `title`s; a screen reader saying
"label" while the tooltip says "tag" is the same confusion with a smaller
audience.

Left alone deliberately: `json_error("invalid label")` and
`"label_ids must be a list"` in `notes/__init__.py` name the `?label=` query
parameter and the `label_ids` request field. Those are wire surface, not the
word a person reads.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K3MMqUtzX1TJgA1oypvm1c
This commit is contained in:
2026-08-31 15:53:28 -04:00
co-authored by Claude Opus 5
parent d838b27518
commit 8c7553d619
7 changed files with 27 additions and 27 deletions
+4 -4
View File
@@ -58,7 +58,7 @@ async def create_label():
data = await request.get_json(silent=True) or {}
name = (data.get("name") or "").strip()
if not name:
return json_error("label name is required", 400)
return json_error("tag name is required", 400)
async with session_scope() as db:
# Idempotent: creating an existing label just returns it.
existing = await db.scalar(select(Label).where(Label.owner_id == g.user_id, Label.name == name))
@@ -81,7 +81,7 @@ async def update_label(label_id: str):
return json_error("nothing to update", 400)
name = (data.get("name") or "").strip() if has_name else None
if has_name and not name:
return json_error("label name is required", 400)
return json_error("tag name is required", 400)
async with session_scope() as db:
label = await _get_owned_label(db, label_id)
if label is None:
@@ -91,7 +91,7 @@ async def update_label(label_id: str):
select(Label).where(Label.owner_id == g.user_id, Label.name == name, Label.id != label.id)
)
if clash is not None:
return json_error("a label with that name already exists", 409)
return json_error("a tag with that name already exists", 409)
label.name = name
if has_color:
label.color = normalize_color(data.get("color"))
@@ -128,7 +128,7 @@ async def merge_label(label_id: str):
if source is None or target is None:
return not_found()
if source.id == target.id:
return json_error("cannot merge a label into itself", 400)
return json_error("cannot merge a tag into itself", 400)
# Notes already carrying the target: a note can't hold the same label twice
# (composite PK), so the source attachment there is just dropped as a dup.
target_notes = set(