Kaynağa Gözat

make the rules findable, and keep the numbers in one table

The file had grown to 578 lines, and 328 of them sat in three sections
written the same way: a rule, then the incident that produced it, in
prose. Readable, and expensive: a search for "what do I do when X" turns
up a paragraph, and every pass pays for all of it.

Device Selection, Firmware Transforms and Effect Names are now lists of
imperatives with the reason after a dash, grouped by what they are about.
Nothing was dropped to make room: every rule, every code reference and
every measurement is still here, and the incidents survive as the clause
that follows the rule they justify.

The measurements moved into their own table, because they are facts about
the world rather than instructions, and several are counts of VIA's
collection that will drift. A rule without the number behind it gets
adjusted on feeling, so the numbers stay — with where each was measured.

The README lost 21 lines it had gained today, by the test its own new
section states: the measured detail behind a fact belongs in this file,
and the README keeps the conclusion and the console transcript.

SKILL.md was behind the tool in one place: it told a user with no keyboard
found to plug the board in, which is the answer the new listing has since
made redundant, and it sent every board without effect names to
`keyboard fetch`, including the ones where a fetch stores a file naming
none.
Paul-Dieter Klumpp 1 hafta önce
ebeveyn
işleme
52a15d9d37
3 değiştirilmiş dosya ile 232 ekleme ve 283 silme
  1. 13 2
      .claude/skills/qmk-rgb/SKILL.md
  2. 204 245
      AGENTS.md
  3. 15 36
      README.md

+ 13 - 2
.claude/skills/qmk-rgb/SKILL.md

@@ -41,8 +41,12 @@ effect all` opens the keyboard, so it fails when none is connected; use
 If the tool is not built:
 > The tool is not currently built. Would you like me to build it and run `qmk-rgb-tool keyboard info`?
 
-If no keyboard is connected:
-> No QMK keyboard is connected. `qmk-rgb-tool keyboard info` lists what is found — plug the board in, or check that it is in wired mode, and I will run it again.
+If no keyboard is found:
+> No keyboard with the QMK Raw HID interface — usage page 0xFF60, usage 0x61 — is
+> connected. `qmk-rgb-tool keyboard info` lists the HID devices it passed over,
+> one line per device with the usage pages each exposes. If the board is among
+> them, its lighting is on a collection this tool does not address; if it is not,
+> it is not plugged in, or it is in wireless mode.
 
 ### 3. When a user wants to change something
 
@@ -108,6 +112,13 @@ names here drifted from the ones Wobkey publishes. `definitions/README.md` says
 which files belong in that directory; a file added there takes effect after a
 rebuild.
 
+A fetch that reports `0 effects` on every channel is the other case, and
+fetching again does not help: the board's definition names VIA's built-in
+lighting menu, and that menu's names are in VIA's own code rather than in any
+file, so there is nothing to read. The Glorious GMMK Pro is one of them. Say so,
+and drive that board by number — `effect <zone> <index>` — instead of sending the
+user after a definition file that will not name anything.
+
 ## Zone behavior
 
 A command that writes lighting names its target as its first argument: one

+ 204 - 245
AGENTS.md

@@ -23,6 +23,17 @@ For the effect catalog specifically, read it from no document at all. Run
 effects than Backlight, and only the tool knows what the current firmware
 supports.
 
+## Keep README.md Short, Because a Person Reads It
+
+A fact belongs in README.md only while it changes what someone types. The
+measured detail behind a fact — how many collections a board reports, which usage
+page a firmware picks, what a filter rejects and why — belongs here, and the
+README may carry it as one sentence or a console transcript, not as the argument.
+Two tests before a paragraph goes in: would a user who only wants to run the
+command have to read it, and is it a fact about the software rather than about
+how the software was arrived at? A README that has to be read twice before the
+command is written has stopped being a reference.
+
 ## Fetch the Remote Before the First Edit
 
 `origin/main` moves while you work, and this repository is worked on from more
@@ -58,142 +69,94 @@ Cross-platform Go CLI library for programmatic/agent-friendly control of QMK key
 
 ## Device Selection
 
-Discovery matches every connected keyboard exposing the QMK Raw HID signature
-(Usage Page `0xFF60`, Usage `0x61`) — the same check `qmk/qmk_udev` performs.
-Nothing has to be registered for a board to work. A keyboard states its own name
-over USB, and the definition file that describes it names its channels; a board
-with neither is still driven, because the channel numbers follow from the QMK
-subsystem they belong to. `keyboards.json` used to supply both and is gone: two
-places to update is how the channel names and the catalog drifted apart.
-
-**An empty discovery has to explain itself, and that is the whole of what
-`DiscoverEvery` is for.** "No keyboard" is what a board that is not connected
-looks like, and it is also what a board that is connected and not reachable looks
-like — a firmware that puts Raw HID on another usage page, a wireless receiver, a
-firmware without Raw HID at all. Nothing in the tool can tell those apart from
-outside, so `keyboard info` prints the HID devices it passed over, one line per
-device with the usage pages it exposes, and the JSON shape carries them in
-`otherHidDevices` on every run.
-
-The two lines above that list say what was looked for and why a board has it, and
-what they may **not** say is that VIA was checked. They did not: this command
-never opens a device, and the filter asks about a usage page, not about VIA. A
-QMK firmware has the collection when Raw HID is enabled and VIA's build cannot be
-compiled without it (`quantum/via.c` errors out), which is a property of the
-firmware and belongs in the sentence as the reason — not as a claim about what
-this command observed. The same goes for "No QMK keyboard found", which is
-overstated anyway: most QMK builds do not enable Raw HID at all, so a plain QMK
-keyboard is a QMK keyboard this tool does not see.
-
-Two decisions there are load-bearing and were measured, not chosen. It is one line
-per **device**, not per collection: a keyboard over USB reports five collections
-(measured on the Impact 80: `0x0001/0x02`, `0x0001/0x01`, `0x0001/0x80`,
-`0x000C/0x01`, `0x0001/0x06`), so per collection buries the one line that says
-something. And in text the list appears **only** when nothing was found: a Mac has
-43 HID collections across 10 devices, none of which is ever a keyboard, and
-printing them beside a successful listing buries the line the user came for. The
-JSON always has them, because a consumer asking "why not this one" needs them
-whether or not the tool found something.
-
-The signature is an exact pair, and it is checked per collection rather than per
-device because macOS reports one HID device per usage pair and Linux one interface
-at a time; the `seen` map is keyed on the path *after* the filter, so a keyboard's
-own keyboard collection cannot consume the raw HID entry. A vendor page near
-`0xFF60` is not a raw HID interface and no guessing is done about intent — the
-firmware picked the page, and saying which page it picked is the answer.
-
-Where a board's own files live is `definitions/` and `profiles/`, both under the
-platform's per-user configuration directory, which `os.UserConfigDir` answers — a
-hardcoded `~/.config` would be wrong on macOS and Windows. `os.UserConfigDir` is
-what names it, and `userDataDir` in `cmd/qmk-rgb-tool/datadir.go` is the only
-place that asks.
-
-Each of the two is one directory, and it is both what is written and what is
-read: `definitionsPath()` and `profilesPath()`, and nothing else. Both were
-searches until they were not — next to the executable, then the working
-directory, then the user's — and the search was a defect in both cases, for the
-same reason in reverse. A definition is written by `keyboard fetch` and read by
-every command, so a directory beside the binary hides the file the user just
-fetched, for as long as the binary is run from a checkout, which is where it is
-run from. A profile is written to the user's directory and read from a
-checkout's, so `load <name>` finds a different file with the same name depending
-on where the command is run, and `save` followed by `load` does not return what
-was saved. A test that a definitions or profiles directory beside the binary is
-ignored, in `TestDataDirectoriesAreNotSearched`, is what stops the search coming
-back.
-
-One argument reaches a file outside those directories, and it is deliberately not
-a search: a profile argument ending in `.json` is a path, handed to the file
-system as written, so `load profiles/lava.json` reads that file. `resolveProfileTarget`
-in `cmd/qmk-rgb-tool/profile.go` is the whole rule — a `.json` suffix is a path,
-anything else is a name in `profilesPath()` — and it has no fallback between the
-two, because a fallback is the search this file rules out. `delete` and `list` stay
-name-only, so there is no way to remove or list a file by path and the per-user
-directory remains the one place a *name* resolves to.
-
-A board's definition has one further source: the file built into the binary, which
-`candidateDefinitions` in `cmd/qmk-rgb-tool/catalog.go` consults after the
-per-user directory.
-
-`keyboard info` does not open the board, so it must not report the board's
-channels: it does not know them, and printing the ones a definition names would
-claim more than it knows. `info` and the effect list open the board and report
-them.
-
-No command validates the zone or `--device` before the command that needs it
-does. A root-level pre-run would make `keyboard info`, `list`, `delete` and
-`completion` demand a keyboard, and the one you run to choose a keyboard cannot
-require that you have chosen one. Where two errors apply, the board wins: a
-keyboard that cannot be selected is reported before a zone name, because the
-vocabulary of a board cannot be known without the board.
-
-`keyboard info` numbers the connected keyboards from 1 in a stable order
-(vendor ID, product ID, path). `--device` accepts that number, never a HID path.
-Without `--device`, commands proceed only when exactly one keyboard is connected;
-otherwise they fail and list the selectable numbers, so a command never targets
-an unintended keyboard. Device numbers are stable for the current session only —
-HID paths are reassigned on reboot and most keyboards report no serial number.
-
-The zone is a **positional argument**, not a flag, and that is not a style choice.
-It was a flag, on the root, and a flag on the root is a flag every help lists:
-`keyboard info` and `list` were told about a channel neither can address, and
-ignored it. An argument is spelled only where it is read, so there is nothing to
-ignore. `zoneArgs` in `cmd/qmk-rgb-tool/main.go` is the one place a *zone*
-argument count is declared — `withZoneArgs` is every command's route into it, and
-`load` calls it directly because its zone is the second argument. Commands that
-take no zone use cobra's own `NoArgs` or `MaximumNArgs`, which is correct: a
-stray token there is a mistyped command, not a missing channel. `zoneArgs` prints
-the usage line on a wrong count, because cobra's own message does not say what
-the missing argument should have been.
-
-The zone takes a VIA lighting channel, named by its QMK subsystem, and a board's
-definition file may name a channel differently. README.md carries the vocabulary.
-Several channels are written comma separated and `all` means every channel the
-keyboard reports; `all` next to another name is `all`, since the union is the
-whole keyboard. Both live in `resolveZoneName` in `cmd/qmk-rgb-tool/zones.go`, and
-so does the fact that a list comes back in channel order however it was written —
-one order for every spelling of one selection, which is what a JSON consumer
-parses. The channels themselves are discovered by asking the keyboard, not
-assumed, and a name that resolves to a channel the board does not have is refused
-by name, every missing one of a list, so nothing is written on the way to a
-failure.
-
-A command that writes lighting cannot be called without a zone, and the argument
-count is what keeps it that way: `brightness` given one argument fails before the
-keyboard is opened. A command that only reads — `info`, `effect` without a name,
-`save` — takes no zone and reports every channel, which is what they are for.
-`load` takes a profile name and at most a zone, and applies the profile to the
-channels the zone names. Write the arity into a new command that writes, or the
-new command will be the one command that writes every channel by default.
-
-The zone is a parameter of `prepareTarget` and `openTarget` rather than a
-package-level variable, which is what makes the rule above enforceable: there is
-no global a test or a command could set by accident, and a command that forgets to
-pass one passes `""`, which means every channel. Only `load` skips a channel that
-does not support an effect, warning on stderr, because there a whole profile is
-being applied. `effect` refuses instead, `all` included: `effect all` asks for
-every channel, so a name only one of them has is a request the tool cannot carry
-out rather than a reason to write the others and report success.
+Rules, with the reason they exist after the dash. The measurements behind them
+are in [Measurements](#measurements); `README.md` carries the user-facing half.
+
+**Discovery**
+
+- Match the QMK Raw HID signature: Usage Page `0xFF60`, Usage `0x61`, the same
+  check `qmk/qmk_udev` performs. Nothing has to be registered for a board; a board
+  states its own name over USB, and a board with no name and no definition file is
+  still driven, because channel numbers follow from the QMK subsystem.
+- The pair is exact and is checked **per collection**, not per device: macOS
+  reports one HID device per usage pair, Linux one interface at a time. Key the
+  `seen` map on the path *after* the filter, or a keyboard's own keyboard
+  collection consumes the raw HID entry. A vendor page near `0xFF60` is not a raw
+  HID interface and no intent is guessed — the firmware picked the page, and
+  saying which page it picked is the answer.
+- An empty result must explain itself. `DiscoverEvery` is the whole of that:
+  `keyboard info` prints the HID devices it passed over, one line per device with
+  the usage pages each exposes, and `otherHidDevices` carries them in the JSON
+  shape on every run. "No keyboard" is what an unplugged board looks like and also
+  what an unreachable one looks like, and nothing outside the tool can tell them
+  apart.
+- Those lines may say what was looked for and why a QMK firmware has that
+  collection; they may **not** say VIA was checked — this command never opens a
+  device, and the filter asks about a usage page. "No QMK keyboard found" is
+  overstated anyway: most QMK builds do not enable Raw HID at all.
+
+**Where a board's files live**
+
+- `definitionsPath()` and `profilesPath()` in `cmd/qmk-rgb-tool/datadir.go` are the
+  only things that say where, and both are the per-user directory `os.UserConfigDir`
+  answers — a hardcoded `~/.config` is wrong on macOS and Windows.
+  `userDataDir` is the only place that asks.
+- One directory each, and it is both what is written and what is read. No search,
+  ever: a directory beside the executable hides the file `keyboard fetch` just
+  wrote, and a checkout's `profiles/` makes `load <name>` read a different file
+  than `save <name>` wrote. `TestDataDirectoriesAreNotSearched` is what keeps the
+  search from coming back.
+- A profile argument ending in `.json` is a path, handed to the file system as
+  written (`resolveProfileTarget`, `cmd/qmk-rgb-tool/profile.go`). No fallback
+  between path and name, because a fallback is the search this file rules out.
+  `delete` and `list` stay name-only.
+- A board's definition has one further source: the file built into the binary with
+  `go:embed` (`definitions/embed.go`), consulted last by `candidateDefinitions` in
+  `cmd/qmk-rgb-tool/catalog.go`, after `--definition` and after the per-user
+  directory. Embedding a vendor's file is not adding a list: one copy of the
+  vendor's own bytes, and `go install` creates no data directory, so without it an
+  installed tool has no definition for the Impact 80.
+
+**Devices and zones**
+
+- `keyboard info` numbers the keyboards from 1 in a stable order (vendor ID,
+  product ID, path). `--device` takes that number, never a HID path. Without it a
+  command runs only when exactly one keyboard is connected, and otherwise fails and
+  lists the numbers — so a command never targets an unintended keyboard. Numbers
+  hold for the current session only: HID paths are reassigned on reboot.
+- No command validates the zone or `--device` before the command that needs it
+  does. A root-level pre-run would make `keyboard info`, `list`, `delete` and
+  `completion` demand a keyboard, and the command you run to choose a keyboard
+  cannot require that you have chosen one. Where two errors apply the board wins:
+  a keyboard's channel vocabulary is not knowable without the keyboard.
+- `keyboard info` must not report the board's channels: it does not open the board
+  and does not know them. `info` and the effect list open it and do know.
+- The zone is a **positional argument**, not a flag — a flag on the root is a flag
+  every help lists, and `keyboard info` and `list` were told about a channel they
+  cannot address and ignored it. `zoneArgs` in `cmd/qmk-rgb-tool/main.go` is the
+  one place a zone count is declared; `withZoneArgs` is every command's route into
+  it and `load` calls it directly because its zone is the second argument.
+  Commands that take no zone use cobra's `NoArgs`/`MaximumNArgs` — a stray token
+  there is a mistyped command, not a missing channel. `zoneArgs` prints the usage
+  line, because cobra's own message does not say what the argument should have been.
+- The zone takes a VIA lighting channel named by its QMK subsystem; a board's
+  definition may name a channel differently. Comma separated lists, `all` for every
+  channel, and `all` next to another name is `all` — all in `resolveZoneName` in
+  `cmd/qmk-rgb-tool/zones.go`, which also returns a list in channel order however it
+  was written, one order for every spelling of one selection. Channels are asked of
+  the keyboard, never assumed, and a name the board does not have is refused by
+  name, every missing one of a list, so nothing is written on the way to a failure.
+- A command that writes lighting cannot be called without a zone, and the argument
+  count is what keeps it that way. A command that only reads — `info`, `effect`
+  without a name, `save` — takes no zone and reports every channel. `load` takes a
+  profile name and at most a zone. Write the arity into a new command that writes,
+  or it becomes the one command that writes every channel by default.
+- The zone is a parameter of `prepareTarget` and `openTarget`, never a package-level
+  variable, so a command that forgets to pass one passes `""` — which means every
+  channel. Only `load` skips a channel that does not support an effect, warning on
+  stderr, because there a whole profile is applied. `effect` refuses instead, `all`
+  included: `effect all` asks for every channel, so a name only one of them has is
+  a request the tool cannot carry out.
 
 ## Stack
 
@@ -226,65 +189,50 @@ Documented behavior must also match the code:
 
 ## Firmware Transforms Values
 
-The Impact 80 firmware rescales or clamps brightness and speed per channel, so
-an accepted 0–255 request is not the value the keyboard holds. `logo` and `side`
-cap brightness at 160 and have only three reachable speeds — 0 freezes the
-animation, 1 is the slowest movement, anything above 1 becomes 4 — while
-`backlight` scales brightness up to 255 and applies speed as given. README.md
-tabulates the per-channel behaviour; the rule that follows from it is the part
-that matters when you write code here:
-
-`brightness`, `speed`, `color` and `effect` read every selected zone
-back and
-print what was actually applied; where all zones match they print the plain
-success line, and where any zone differs they print one summary line naming each
-zone's real value and the request. Never let a command report success for a
-value the keyboard did not accept. All of them read back through the shared
-`readBackValues`, so a future parameter needs no new read-back path — but the
-helper above it is not as shared as it looks. `setValueVerified` writes one
-value to every channel, which fits `brightness` and `speed` and fits nothing
-else: `effect` carries a different ID per channel, because one effect name is a
-different index on each subsystem, and so has its own `setEffectVerified`.
-Assume the next parameter needs its own wrapper, and share only the read.
-
-A board can refuse an ID that looks valid. The Impact 80's `logo` and `side`
-channels read effect ID 0 as "lighting off" and leave the mode register alone,
-so `effect logo none` there does nothing to the effect and is reported as the
-effect still running. `disable` is the command that turns a channel off, because
-it also writes brightness 0. Above the top the behaviour is the opposite: the
-backlight channel does not refuse ID 47 or ID 99 but clamps it to the 46 it
-holds, so an effect index there is reported as the index the keyboard ended
-  up with. Read
-the register back; never report the number that was asked for.
-
-There is no hand-written effect catalog, and that is deliberate. A board's names
-come from its VIA definition file, read at runtime, and the Impact 80's file is
-vendored in `definitions/`. A hand-written list beside the vendor's file is two
-places to update one name, which is exactly how the two drifted apart: the
-compiled-in list spelled `breathing` and `fixed_wave` where the file writes
-`breathe` and `fixed wave`, and the file spells the same effect two ways on two
-channels of one board. Do not add such a list back, and do not "correct" a
-definition file either.
-
-**Embedding a vendor's file is not adding a list.** `definitions/embed.go` builds
-the JSON files in that directory into the binary with `go:embed`, and
-`candidateDefinitions` in `cmd/qmk-rgb-tool/catalog.go` consults them last, after
-`--definition` and after a file in a definitions directory. The distinction is
-the one that matters: the bytes are the vendor's own file, there is exactly one
-copy of it in the repository, and nothing transcribes it. A file the user places
-still wins, so the built-in copy is a fallback and not a second source a name
-could drift from. `go install` copies a binary to `$GOPATH/bin` and creates no
-data directory, so without the embed an installed tool has no definition for the
-Impact 80 at all — and that board is not in VIA's collection, so there is nothing
-to fetch either.
-
-The file names only the effects a board's firmware implements, so it may stop
-short of the board's highest ID: this one's backlight channel ends at 45 while
-the board takes 46, which is left `unknown` on purpose. A name no source publishes
-is a name the tool would be inventing. The live register proves which IDs a board
-takes, never which name belongs to one.
-
-When you see a name or a behavior in one file, grep for it across the whole repo before deciding if a change is consistent.
+The Impact 80 firmware rescales or clamps brightness and speed per channel, so an
+accepted 0–255 request is not the value the keyboard holds. README.md tabulates
+the per-channel behaviour; the rule that follows from it is the part that matters
+when you write code here:
+
+- `brightness`, `speed`, `color` and `effect` read every selected zone back and
+  print what was actually applied; where all zones match they print the plain
+  success line, where any differs they print one summary line naming each zone's
+  real value and the request. Never let a command report success for a value the
+  keyboard did not accept.
+- All of them read back through the shared `readBackValues`, so a future parameter
+  needs no new read-back path — but the helper above it is not as shared as it
+  looks. `setValueVerified` writes one value to every channel, which fits
+  `brightness` and `speed` and fits nothing else: `effect` carries a different ID
+  per channel, because one effect name is a different index on each subsystem, and
+  so has its own `setEffectVerified`. Assume the next parameter needs its own
+  wrapper, and share only the read.
+- A board can refuse an ID that looks valid. The Impact 80's `logo` and `side`
+  channels read effect ID 0 as "lighting off" and leave the mode register alone,
+  so `effect logo none` there does nothing to the effect and is reported as the
+  effect still running. `disable` is the command that turns a channel off, because
+  it also writes brightness 0. Above the top the behaviour is the opposite: the
+  backlight channel does not refuse ID 47 or ID 99 but clamps it to the 46 it
+  holds, so an effect index there is reported as the index the keyboard ended up
+  with. Read the register back; never report the number that was asked for.
+- There is no hand-written effect catalog, and that is deliberate. A board's names
+  come from its VIA definition file, read at runtime. A hand-written list beside the
+  vendor's file is two places to update one name, which is exactly how the two
+  drifted apart: the compiled-in list spelled `breathing` and `fixed_wave` where
+  the file writes `breathe` and `fixed wave`. Do not add such a list back, and do
+  not "correct" a definition file either.
+- **Embedding a vendor's file is not adding a list.** `definitions/embed.go` builds
+  the JSON files in that directory into the binary with `go:embed`, and
+  `candidateDefinitions` consults them last, after `--definition` and after a file
+  in a definitions directory. The distinction is the one that matters: the bytes
+  are the vendor's own file, there is exactly one copy of it in the repository,
+  and nothing transcribes it. A file the user places still wins, so the built-in
+  copy is a fallback and not a second source a name could drift from.
+- A file names only the effects a board's firmware implements, so it may stop
+  short of the board's highest ID: this one's backlight channel ends at 45 while
+  the board takes 46, which is left `unknown` on purpose. A name no source
+  publishes is a name the tool would be inventing.
+- When you see a name or a behavior in one file, grep for it across the whole repo
+  before deciding if a change is consistent.
 
 ## Read the Firmware When Something Is Unclear
 
@@ -329,12 +277,11 @@ step: `CatalogFor` is gone, and a command that reached for a catalog itself woul
 silently ignore a file the user had placed, so do not add one.
 
 Two things about reading those files are easy to get wrong, and both were
-measured on VIA's own collection of 2029 definitions rather than assumed:
+measured on VIA's own collection rather than assumed:
 
-- An option's number is not its position. 194 options carry a number that differs
-  from where they stand, so a board can name its first effect as ID 1 and have
-  no ID 0 at all. `internal/rgb/definition.go` reads the number and falls back to
-  the position only when there is none. This is why `Catalog` holds
+- An option's number is not its position, so a board can name its first effect as
+  ID 1 and have no ID 0 at all. `internal/rgb/definition.go` reads the number and
+  falls back to the position only when there is none. This is why `Catalog` holds
   `[]Effect{ID, Name}` and not a `[]string` indexed by ID.
 - A definition names its channels too, in the sub-menu it puts each one under,
   and those names are what VIA shows, so they are the display name. The QMK
@@ -351,11 +298,11 @@ The files are VIA's format, but their addressing is QMK's, and that is not a
 choice this tool made. The HID interface, the channel numbers, the lighting
 value IDs and the whole channel probe are QMK's; only the names come from the
 file, because the VIA protocol cannot carry names and VIA reads them from a
-file for the same reason. Measured over VIA's collection, all 190 effect
-dropdowns there use a QMK value key, so keying the parser on `id_qmk_*_effect`
-loses nothing today. `effectValueKeys` in `internal/rgb/definition.go` is the
-only place to register a vendor's own key, and a board that invents one gets no
-names until it is added there.
+file for the same reason. All effect dropdowns in VIA's collection use a QMK
+value key, so keying the parser on `id_qmk_*_effect` loses nothing today.
+`effectValueKeys` in `internal/rgb/definition.go` is the only place to register a
+vendor's own key, and a board that invents one gets no names until it is added
+there.
 
 A definition writes the manufacturer's spelling, which need not be the tool's,
 so the alias table is a package default every catalog gets rather than part of
@@ -364,10 +311,7 @@ aliases, and to an alias of it, and `byName` treats a space and an underscore as
 the same character, because that is the whole difference between a
 manufacturer's display spelling and the tool's identifier — `fixed wave` and
 `fixed_wave` are one effect, and a documented name may not stop working because a
-definition file is present. Without the last of those, the Impact 80's
-profile stops loading the moment a definition file is placed: the file writes
-`breathe`, the compiled-in catalog writes `breathing`, and only one of them is
-present at a time.
+definition file is present.
 
 A definition names the effects a board's firmware implements, so it may cover
 fewer IDs than the board takes. A file replaces the built-in copy for its
@@ -376,19 +320,15 @@ file stops at ID 45, so no name for ID 46 is reachable while that file is
 present, built in or placed. README.md carries the user-facing half of this.
 
 There is no shared database of effect names to be had, and do not plan around
-one. The closest thing is VIA's collection, `the-via/keyboards`: 2029
-definitions, one per unique VID/PID, of which 179 name VIA's built-in
-`qmk_rgb_matrix` menu and 163 ship their own `id_qmk_rgb_matrix_effect`
-dropdown, which is where a board's effect names actually live — not under a
+one. The closest thing is VIA's collection, `the-via/keyboards`, and a board's
+names live in an `id_qmk_rgb_matrix_effect` dropdown under `menus` — not under a
 `lighting` or `effects` key, so a grep for those finds nothing and looks like
-proof that no bulk source exists. The 153 that deviate are not merely shorter
-lists: they reorder IDs and spell names as QMK enum identifiers or as numbered
-display labels, and only 10 are an exact prefix of the built-in 45. So a
-subsystem default is wrong for most boards, and a board's names come from the
+proof that no bulk source exists. Most boards deviate from the built-in list, so
+a subsystem default is wrong for most of them, and a board's names come from the
 definition its own vendor publishes. OpenRGB drives the same raw HID interface
 but keeps its device data in C++ controllers, so it settles VID/PID, not effects.
-README.md carries the details; the conclusion for code here is that a catalog is
-transcribed per board and cannot be generated from a common source.
+The conclusion for code here is that a catalog is transcribed per board and
+cannot be generated from a common source.
 
 **The names for VIA's built-in menu are in no definition file at all.** A
 definition that writes `"menus": ["qmk_rgb_matrix"]` — a string, not a menu object —
@@ -411,42 +351,39 @@ projects drive the same raw HID interface from a terminal, and each of them take
 an effect **ID**:
 
 - <https://github.com/FrameworkComputer/qmk_hid> — Rust CLI, plus a Python GUI in
-  `python/`; BSD-3-Clause. `qmk_hid via --rgb-effect 38`, plus `--rgb-brightness`,
-  `--rgb-hue`, `--rgb-saturation`, `--rgb-color`, `--rgb-effect-speed`,
+  `python/`; BSD-3-Clause. `qmk_hid via --rgb-effect 38`, `--rgb-brightness`,
+  `--rgb-hue`, `--rgb-saturation`, `--rgb-effect-speed`, `--rgb-color`,
   `--backlight`, `--backlight-breathing`, `--save`, `--device-indication`,
-  `--eeprom-reset`, `--bootloader`, and `-l`/`--vid`/`--pid`. Its README states
-  the reason out loud: "the effect numbers can be different per keyboard", and it
-  says the tool "will soon be superceded by QMK XAP".
+  `--eeprom-reset`, `--bootloader`, `-l`/`--vid`/`--pid`. Its README states the
+  reason out loud: "the effect numbers can be different per keyboard", and it says
+  the tool "will soon be superceded by QMK XAP".
 - <https://github.com/njkevlani/qmk-light> — C++ against hidapi, one `qmk-light.cpp`;
-  **no licence file**, so nothing may be taken from it. `--list`, `--get-brightness`,
-  `--set-brightness` (absolute or `+5`/`-10`), `--list-effects`, `--get-effect`,
-  `--set-effect`, `--get-effect-speed`, `--set-effect-speed`, `--get-color`,
-  `--set-color h,s`, `--device <index|path>`, `--first-device`, `--quiet`.
+  **no licence file**, so nothing may be taken from it. `--list`,
+  `--get-brightness`, `--set-brightness` (absolute or `+5`/`-10`),
+  `--list-effects`, `--get-effect`, `--set-effect`, `--get-effect-speed`,
+  `--set-effect-speed`, `--get-color`, `--set-color h,s`,
+  `--device <index|path>`, `--first-device`, `--quiet`.
 - <https://github.com/Drugantibus/qmk-hid-rgb> — Python, GPL-3.0, untouched since
   2021; a proof of concept that needs a keymap of its own with `RAW_ENABLE = yes`
   and the board's VID/PID written into the source.
 
 None of them reads a definition file, so none can name an effect. That is the gap
-this tool's catalog and `keyboard fetch` sit in.
-
-Read them for that gap, and for anything this tool does not do yet: they are the
-three live implementations of the same wire protocol, and their flag surfaces are
-where a gap in ours shows up first — persistence (`--save`, `--eeprom-reset`),
-device indication, effect speed as its own parameter, colour by name, jumping to
-the bootloader, and explicit device selection where more than one board is
-attached (`--first-device`). Any of those is a candidate feature here, not a
-duplicate to reimplement badly. Check a project's licence before taking code from
-it, and check whether it has moved on before treating its behaviour as current.
+this tool's catalog and `keyboard fetch` sit in. Read them for that gap, and for
+anything this tool does not do yet: their flag surfaces are where a gap in ours
+shows up first — persistence (`--save`, `--eeprom-reset`), device indication,
+effect speed as its own parameter, colour by name, jumping to the bootloader, and
+explicit device selection where more than one board is attached (`--first-device`).
+Any of those is a candidate feature here, not a duplicate to reimplement badly.
+Check a project's licence before taking code from it, and check whether it has
+moved on before treating its behaviour as current.
 
 **The `qmk` CLI has never had a lighting command.** Do not explain a missing
-feature by saying QMK removed one. Measured on `qmk_firmware`: `lib/python/qmk/cli/`
-carries no `led`, `rgblight` or `hid` module at tags 0.6, 0.9, 0.10, 0.15 through
-0.21, 0.24 or on master, the commits API returns zero commits for those paths, and
+feature by saying QMK removed one. `lib/python/qmk/cli/` carries no `led`,
+`rgblight` or `hid` module at tags 0.6, 0.9, 0.10, 0.15 through 0.21, 0.24 or on
+master, the commits API returns zero commits for those paths, and
 `docs/cli_commands.md` documents none at any of those tags. The path filter does
-report deletions — `lib/python/qmk/cli/cformat.py` and `multibuild.py` both end at
-`4723f308a`, *"Remove CLI commands: `multibuild`, `cformat`, `fileformat`,
-`pyformat`"*, 2023-01-18 — so an empty result means the file was never there, not
-that it was removed.
+report deletions, so an empty result means the file was never there, not that it
+was removed.
 
 ## Output Shape
 
@@ -481,6 +418,28 @@ All documentation (README.md, comments, AGENTS.md) must stay in sync with the co
 When code and documentation conflict, **ask the user** before deciding which one to change.
 Do not silently pick a winner — explicitly state the conflict and get direction.
 
+## Measurements
+
+Facts about the world that a rule above rests on. They are recorded here because
+the rules are what someone needs while editing, and because a rule without the
+number behind it tends to be adjusted on feeling. Re-measure before quoting one
+twice; several are counts of a collection that moves.
+
+| What | Measured | Where |
+|---|---|---|
+| VIA's collection | 2029 definitions, one per unique VID/PID | `the-via/keyboards`, tree API |
+| …name VIA's built-in `qmk_rgb_matrix` menu | 179 boards | as above |
+| …ship their own `id_qmk_rgb_matrix_effect` dropdown | 163 boards, of which 10 are an exact prefix of the built-in 45 | as above |
+| …of those, write names as QMK enum identifiers | 11; the other 152 use display labels | as above |
+| Effect options whose number is not their position | 194 | as above |
+| Effect dropdowns in the collection that use a QMK value key | 190, which is all of them, so keying on `id_qmk_*_effect` loses nothing today | as above |
+| HID collections per device | 5 on the Impact 80 (`0x0001/0x02`, `0x0001/0x01`, `0x0001/0x80`, `0x000C/0x01`, `0x0001/0x06`) | macOS, this Mac, `qmk-rgb-tool`-adjacent enumeration |
+| HID devices a Mac reports that are never a keyboard | 43 collections across 10 devices, Impact 80 attached | macOS, same run |
+| `qmk` CLI lighting commands | none, at any tag — see the rule above | `qmk/qmk_firmware`, contents + commits API per path; the path filter does report deletions, e.g. `4723f308a` removes `cformat` and `multibuild` |
+| Impact 80 brightness and speed per channel | `logo`/`side` cap at 160 with three reachable speeds (0 freezes, 1 slowest, above 1 becomes 4); `backlight` scales to 255 | README tabulates it; measured on the board |
+| Impact 80 effect IDs | `backlight` takes 0–46 and clamps above; `logo`/`side` take 0–6 and read ID 0 as off | measured on the board |
+| VIA's built-in menu names | 45, `All Off` … `Solid Multi Splash`, at IDs 0–44 | `the-via/reader/src/common-menus/qmk_rgb_matrix.ts` |
+
 ## Go Development
 
 ### Tooling

+ 15 - 36
README.md

@@ -297,14 +297,10 @@ report no serial number. Re-run `keyboard info` after reconnecting a keyboard
 rather than storing the number.
 
 A keyboard is one HID collection with usage page `0xFF60` and usage `0x61`, the
-QMK Raw HID signature `qmk/qmk_udev` matches on. A QMK firmware has one when Raw
-HID is enabled, and VIA's build cannot be compiled without it — but the two are
-not the same thing, and a board with Raw HID but no VIA is found here and then
-fails the channel probe with a read timeout, because nothing answers a VIA
-command. Nothing else is addressed, and when nothing matched, `keyboard info`
-lists the HID devices that *are* connected with the usage pages they expose —
-because a keyboard that is plugged in and not reachable looks exactly like one
-that is not plugged in:
+QMK Raw HID signature `qmk/qmk_udev` matches on. Nothing else is addressed, so when
+nothing matched, `keyboard info` lists the HID devices that *are* connected, one
+line per device with the usage pages each exposes — a keyboard that is plugged in
+and not reachable looks exactly like one that is not plugged in:
 
 ```console
 $ qmk-rgb-tool keyboard info
@@ -317,21 +313,10 @@ These HID devices are connected, and none of them has that collection:
   0x0000/0x0000  no product string     0xFF00/0xFF
 ```
 
-The first line says what was looked for, the second why a board has it, and
-neither says VIA was checked: nothing is opened here, and the filter asks about
-the collection, not about VIA.
-
-One line per device, not per collection: a keyboard over USB typically has five
-collections (keyboard, consumer control, mouse, vendor), and one line each would
-bury the one that says something — a vendor page that is `0xFF80` where it would
-have to be `0xFF60` is a board this tool cannot address, which is a different
-answer than a board in wireless mode or on a firmware without Raw HID. The
-firmware decides that page, so nothing here is guessed.
-
-The same list is in `keyboard info --json` as `otherHidDevices`, in every run and
-not only an empty one, so a consumer can ask "which keyboards" and "why not this
-one" from one shape. It is never printed beside a successful listing: a Mac has
-around forty HID devices that are never a keyboard.
+A vendor page that is `0xFF80` where it would have to be `0xFF60` is a board this
+tool cannot address. The same list is in `keyboard info --json` as
+`otherHidDevices`, in every run and not only an empty one, and is never printed
+beside a successful listing.
 
 ## Color Notations
 
@@ -496,10 +481,9 @@ and has 45 names, `All Off` through `Solid Multi Splash`, at IDs 0 to 44.
 
 **A board that names the built-in menu has no effect names in this tool.** Its
 definition file carries the name of VIA's own menu, not the names inside it, so
-there is nothing in the file to read and nothing is invented. The fetch succeeds
-and the file is stored, the channel list comes back with `0 effects` on every
-channel, and an effect is set by number. The Glorious GMMK Pro (0x320F/0x5044) is
-one of these boards, and so is every GMMK V2:
+there is nothing to read, the fetch succeeds, and the channel list comes back
+with `0 effects` on every channel. The Glorious GMMK Pro (0x320F/0x5044) and
+every GMMK V2 are such boards, and an effect is set there by number:
 
 ```console
 $ qmk-rgb-tool keyboard fetch
@@ -516,15 +500,10 @@ names are not in the file, and an effect is set by number with `effect <zone> <i
 ```
 
 `0 effects` next to a saved definition is that answer, not a failed fetch. The 45
-names are public, and for a board like this the IDs are their positions, so
-nothing would have to be measured. This tool does not fall back to them, for two
-reasons. A list transcribed into the source is the second copy of a name that a
-per-board file exists to be, and the spellings do not line up anyway: `All Off`
-and `Band Sat.` are display labels that do not reach `none` and `band_sat` the way
-`Solid Color` and `Breathing` do. A board in this group is driven by number until a
-definition spells its effects out — see
-[Compatibility Aliases](#compatibility-aliases) for what the tool accepts for a
-board that does.
+names are public, and this tool does not fall back to them: a transcribed list is
+the second copy of a name a per-board file exists to be, and the spellings do not
+line up anyway — `All Off` and `Band Sat.` reach neither `none` nor `band_sat`.
+See [Compatibility Aliases](#compatibility-aliases) for what such a board accepts.
 
 The other 153 deviate, and not only in how many effects they compile in. MonsGeek's
 M1 uses QMK's enum identifiers and its own 19: `SOLID_COLOR`, `BREATHING`,