A reserved key comes to life — the card window's sidebar grows a Labels section, and labels stops being somebody else's

`labels` has been a reserved tracker key since the rewrite: preserved verbatim, never
interpreted, drawn only as an anonymous row in the Details section beside `assignees` and
`due`. The owner's cards claim it for first-party use, so it joins the schema — read
leniently (a list of names, a bare scalar coercing to one, a mapping malformed and
preserved), written canonically (a quoted flow list in the order the user arranged, no
auto-sort), and removed outright when the last label goes, the way an expanded lane drops
`collapsed`.

Identity is case-insensitive and display is case-preserving, so a card carries `bug` once
however many ways the board spells it, and entries the reading cannot name ride through the
write untouched at the tail.

The section sits second, above Details — which is the point rather than a layout preference:
Details is where keys the app does *not* own are shown, and this key just stopped being one.
Rows rather than chips, because the sidebar is twenty-six characters wide. The add field
autocompletes against the board's own used-labels universe, derived from every live and
trashed card with no store beside the files, and says out loud when Return would mint a word
the board has never used.

Writes ride a `.relabel` operation of their own, because the commit composer has said
"Relabel card 'X'" since long before there was a control to press — and now the undo row says
it too.

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
This commit is contained in:
2026-08-09 12:06:01 -04:00
parent fd31ed24f4
commit da37ed61bf
24 changed files with 1928 additions and 29 deletions
+22 -2
View File
@@ -745,6 +745,16 @@ enum ChangeNarrator {
/// `created`, `modified-by` and the on-touch heal's backfilled `kind` are all schema-owned, and a
/// diff touching only those composes nothing (06 the bookkeeping rule).
///
/// **`labels` is the one key added back by hand** (2026-08-09): it joined `schemaOwned` when the
/// owner claimed it for first-party use (`FrontmatterKeys.labels`), which took it out of
/// `unknownFields` and would have taken the **Relabel** event with it the one event here that
/// 06-history-undo.md names verbatim, and which predates the control by months precisely because
/// the composer is origin-agnostic. Being schema-owned changes where the *sidebar* renders the key
/// and nothing about whether a change to it is worth narrating; an agent, a tracker sync and the
/// app's own submenu all still move it, and all three still say "Relabel". `assignees` and `due`
/// are untouched they are still reserved, still unknown keys, and still arrive through the
/// dictionary above.
///
/// Values compare **as written** (`rawValue`) rather than as parsed YAML: the composer's business
/// is that the file changed, and re-deriving an equivalence between `[a, b]` and `[a,b]` would be
/// a second YAML semantics to keep honest.
@@ -757,8 +767,18 @@ enum ChangeNarrator {
kinds: (label: Kind, assignee: Kind, due: Kind, customKey: Kind),
paths: [String]
) -> [Event] {
let before = Dictionary(previous.unknownFields.map { ($0.key, $0.rawValue) }, uniquingKeysWith: { _, last in last })
let after = Dictionary(current.unknownFields.map { ($0.key, $0.rawValue) }, uniquingKeysWith: { _, last in last })
var before = Dictionary(previous.unknownFields.map { ($0.key, $0.rawValue) }, uniquingKeysWith: { _, last in last })
var after = Dictionary(current.unknownFields.map { ($0.key, $0.rawValue) }, uniquingKeysWith: { _, last in last })
// `labels`, put back by hand see the note above. Absent stays absent (no entry), and the
// text is `FrontmatterField.rawValue`'s own rule restated, so a key with no span of its own
// (an uneditable whole-frontmatter flow mapping) compares by the parse's rendering exactly as
// its neighbours in the dictionary do.
func labelsText(of document: FrontmatterDocument) -> String? {
guard let value = document.value(for: Keys.labels) else { return nil }
return document.rawValue(for: Keys.labels) ?? value.description
}
before[Keys.labels] = labelsText(of: previous)
after[Keys.labels] = labelsText(of: current)
guard before != after else { return [] }
func phrase(_ verb: String, _ render: (String) -> String) -> String {