Extract the gesture vocabulary from the code that implements it

Every gesture the application has was documented in the comment beside
the `TouchArea` that implements it. Excellent comments, and unreachable
by anyone not reading the source — which is the FR-UI-4 failure in a
different costume: a gesture nobody can find is a feature only its author
knows about.

Writing them out again in a hand-kept help page is the failure this
avoids. Two descriptions of one gesture drift, and it is always the prose
that drifts: the code is exercised every time somebody uses the
application and the page is exercised never. A help screen confidently
describing a double tap the grid stopped honouring last week is worse
than no help screen — and the grid did stop honouring one, in the commit
before this.

So the comment beside the implementation stays the only copy, and a
`GESTURE:` block beside it is scanned into two artefacts: `docs/gestures.md`
for a reader, and a Rust table for the application to draw a help sheet
from. Both committed, both gated, so neither can quietly stop describing
the code.

It lives in the traceability crate because it is the same operation on
the same input — walk the tree, pull structured tags out of comments,
render, fail if the committed artefact has moved. Only the vocabulary is
new. It scans `ui` and `apps` alone: a gesture needs an interface to be
performed on, and excluding `tools` is also what stops the scanner
extracting its own worked examples as broken gestures.

Fifteen gestures so far, across the library grid and the People screen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-30 00:09:49 +02:00
co-authored by Claude Opus 5
parent 20c368d3fc
commit 31a3580f9d
6 changed files with 1012 additions and 0 deletions
+32
View File
@@ -127,6 +127,14 @@ component FaceCell inherits Rectangle {
}
}
// GESTURE: Pull a face out of the wrong person
// where: People
// touch: Tap the faces that do not belong, then "Split off"
// pointer: Click the faces that do not belong, then "Split off"
// why: Grouping over-merges on siblings, on parents and
// children, and on the same person a decade apart, so
// splitting is as prominent as merging. A tool that can
// only merge makes its own errors permanent.
TouchArea {
clicked => { root.toggle-pick(); }
}
@@ -139,6 +147,14 @@ component FaceCell inherits Rectangle {
// Only a suggestion needs ruling on. Once confirmed, the pair
// collapses to the label — there is nothing left to decide, and
// leaving the buttons would invite an accidental un-confirm.
// GESTURE: Rule on a suggested face
// where: People
// touch: Tick to confirm it, cross to reject it
// pointer: Tick to confirm it, cross to reject it
// why: A face is either the system's guess or the user's
// judgement, and the two are never conflated. A
// rejection is remembered, so the face is not suggested
// for that person again.
if !face.confirmed: IconButton {
icon: "check";
clicked => { root.confirm(); }
@@ -526,6 +542,13 @@ export component IdentityScreen inherits Rectangle {
// The way back from a face to the photographs. This is the
// point of having identified anybody, and without it the
// screen is a filing cabinet with no drawer handles.
// GESTURE: See a person's photographs
// where: People
// touch: Choose them in the rail, then "Show photos"
// pointer: Choose them in the rail, then "Show photos"
// why: This is the point of having identified anybody.
// Without it the screen is a filing cabinet with no
// drawer handles.
if root.selected-person >= 0: Button {
text: "Show photos";
clicked => { root.show-photos(root.selected-person, false); }
@@ -556,6 +579,15 @@ export component IdentityScreen inherits Rectangle {
// next to the button that applies them and the rail that shows
// what they did, and a value changed three screens away from
// its effect is a value nobody can tune.
// GESTURE: Change how faces are grouped
// where: People
// touch: "Grouping…", move the dials, then Regroup
// pointer: "Grouping…", move the dials, then Regroup
// why: The right match confidence is a property of your
// library, not of the model. "What would this do?"
// answers for this library without writing
// anything; names, confirmations and the groups you
// have set aside are kept whatever the dials say.
Button {
text: "Grouping…";
active: root.grouping-open;