Skip to content

Commit 923e562

Browse files
committed
Clear both red audits, unify the re-send label, fix two bugs in the selector
icon-audit 2 findings -> 0, row-actions-audit 1 -> 0. Both exit 0 now. The row actions fix is the one its sibling already made. design.md settles that a row reaching three actions gets the menu, and /admin/barcode_items carried View, Edit and Delete -- but /barcode_items had the identical shape and resolved it the cheaper way, with the reasoning recorded in the partial: the View goes because the record's name in the first cell links to it, leaving a settled pair that is always both present, so they stay inline. Two clicks saved over a menu, and a column that cannot move because nothing in it is conditional. Collapsing to a menu would have satisfied the rule and left two parallel tables looking different for no reason. design.md now records dropping to two as an accepted resolution beside "gets the menu", since the rule is about reaching three and not about the menu being the only cure. The two icon findings were unregistered rather than wrong. The lexicon is a meaning->glyph registry whose own note says adding one has to be a decision rather than a reflex, so `bi-key` = reset a password and `bi-arrow-left-right` = compare with another period are now in it. And the lexicon had already drifted from design.md in exactly the way its note warns against -- "change both together" -- with six entries in the JSON and absent from the prose table: bi-people, bi-person-gear, bi-filetype-csv, bi-sliders, bi-clipboard, bi-calculator. All eight are in both now, checked in both directions. The label is "Re-send invitation" in all three places. Chosen because the code and the user-facing noun already say invitation everywhere -- invitation_status, reinvitable?, resend_user_invitation_path -- so consistency with the domain beats a two-to-one majority for "invite". The dated mockup keeps the old wording as a record. which-audits.rb selected all 30 and all 30 were run, which is the point. All clean: axe 0 violations over 160 pages, keyboard no findings, responsive no findings, page-audit 0 defects, template-compile all compile, dead-code at its documented 147. form-validation names /partners/family_requests/new under "7 forms an empty submit does not exercise" -- informational, not a finding. Using the tool found two defects in the tool: * it printed `ruby bin/design/dead-code.rb`, which fails. Three Ruby audits need the app booted and they are exactly the three that mention `Rails.`, so the runner is inferred now and a new Rails-using audit gets it for free. * the pasteable command reported only each audit's last four lines, so it prints `exit=` too -- several of these say something reassuring in prose while exiting non-zero, and dead-code exits 1 by design. 81 examples 0 failures; erb_lint and rubocop clean; state.rb --check up to date. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent f6fb04e commit 923e562

8 files changed

Lines changed: 74 additions & 18 deletions

File tree

app/views/admin/barcode_items/_barcode_item_row.html.erb

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,23 @@
11
<tr>
2-
<td><%= barcode_item_row.barcodeable.name %></td>
2+
<td class="font-medium text-slate-900">
3+
<%= link_to barcode_item_row.barcodeable.name, admin_barcode_item_path(barcode_item_row), class: "link-brand" %>
4+
</td>
35
<td><%= barcode_item_row.quantity %></td>
46
<td><%= barcode_item_row.value %></td>
57
<td class="cell-actions">
6-
<div class="flex justify-end gap-1.5">
7-
<%= essentials_row_icon_link "View", admin_barcode_item_path(barcode_item_row), icon: "bi-eye" %>
8+
<%# Was View, Edit and Delete inline, which design.md settles: "if any row in a table can reach
9+
three actions, every row in that table gets the menu". `row-actions-audit` reported it and
10+
the report went unread while the audit was noisy for an unrelated reason.
11+
12+
The fix is the one its own sibling already took -- `/barcode_items` had this exact shape and
13+
the reasoning is recorded there: the View goes because the name in the first cell links to
14+
the record, which leaves a settled pair that is always both present, so they stay inline.
15+
Two clicks saved over a menu, and the column cannot move because nothing here is
16+
conditional. Collapsing to a menu would have satisfied the rule and left the two tables
17+
looking different for no reason. %>
18+
<div class="flex justify-end gap-1.5 whitespace-nowrap">
819
<%= essentials_row_icon_link "Edit", edit_admin_barcode_item_path(barcode_item_row), icon: "bi-pencil" %>
920
<%= essentials_row_icon_action "Delete", admin_barcode_item_path(barcode_item_row), method: :delete, icon: "bi-trash", tone: :danger,
10-
1121
confirm: confirm_delete_msg(barcode_item_row.item.name) %>
1222
</div>
1323
</td>

app/views/admin/users/_list.html.erb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
<td class="cell-actions">
2323
<div class="flex justify-end gap-1.5">
2424
<% if user.invitation_sent_at.present? %>
25-
<%= essentials_row_icon_action "Re-send invite", admin_user_resend_invitation_path(user), method: :post, icon: "bi-envelope",
25+
<%= essentials_row_icon_action "Re-send invitation", admin_user_resend_invitation_path(user), method: :post, icon: "bi-envelope",
2626
confirm: "Re-send an invitation to #{user.preferred_name}?" %>
2727
<% end %>
2828
<%= essentials_row_icon_link "Edit", edit_admin_user_path(user), icon: "bi-pencil" %>

app/views/partners/_partner_row.html.erb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
end
3333
when "invited"
3434
[{label: "Review profile", path: partner_path(partner_row, anchor: "partner-information"), icon: "bi-eye"},
35-
{label: "Re-send invite", path: invite_partner_path(partner_row), icon: "bi-envelope", method: :post,
35+
{label: "Re-send invitation", path: invite_partner_path(partner_row), icon: "bi-envelope", method: :post,
3636
confirm: "Re-send an invitation to #{partner_row.name}?"}]
3737
when "awaiting_review"
3838
[{label: "Review profile", path: partner_path(partner_row, anchor: "partner-information"), icon: "bi-eye"}]

bin/design/icon-lexicon.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,9 @@
3535
"adjust an allocation": "bi-sliders",
3636
"copy to the clipboard": "bi-clipboard",
3737
"total something up": "bi-calculator",
38-
"more actions": "bi-three-dots-vertical"
38+
"more actions": "bi-three-dots-vertical",
39+
"compare with another period": "bi-arrow-left-right",
40+
"reset a password": "bi-key"
3941
},
4042
"structural": [
4143
"bi-x-lg",

bin/design/which-audits.rb

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -166,16 +166,23 @@ def expand(tokens, file)
166166
exit 0
167167
end
168168

169+
# How to invoke each one. Three Ruby audits need the app booted -- `dead-code`, `dead-routes` and
170+
# `template-compile-audit` -- and the first draft of this script printed `ruby bin/design/dead-code.rb`,
171+
# which simply fails. Inferred rather than listed: those three are exactly the ones that mention
172+
# `Rails.`, and the four that run standalone never do, so a new audit reaching for Rails gets the
173+
# runner without anyone maintaining a list. Verified by running all seven both ways. A wrong guess
174+
# fails on the first line of output, which is the acceptable direction for it to be wrong in.
175+
def runner_for(name)
176+
return "pw" if name.end_with?(".js")
177+
return "python3" if name.end_with?(".py")
178+
179+
File.read(File.join(ROOT, "bin/design", name)).include?("Rails.") ? "bin/rails runner" : "ruby"
180+
end
181+
169182
puts "Run these #{selected.size}:"
170183
selected.sort_by { |name, hits| [-hits.size, name] }.each do |name, hits|
171-
runner = if name.end_with?(".js")
172-
"pw"
173-
elsif name.end_with?(".py")
174-
"python3"
175-
else
176-
"ruby"
177-
end
178-
puts format(" %-34s matched %d: %s", "#{runner} bin/design/#{name}", hits.size, hits.first(3).join(", "))
184+
puts format(" %-42s matched %d: %s", "#{runner_for(name)} bin/design/#{name}", hits.size,
185+
hits.first(3).join(", "))
179186
end
180187

181188
quiet = audits.keys - selected.keys
@@ -190,9 +197,18 @@ def expand(tokens, file)
190197
puts
191198
puts "All of them, sequentially, output per audit in /tmp:"
192199
puts
193-
puts " for a in #{selected.keys.sort.join(" ")}; do \\"
194-
puts " case $a in *.js) r=pw;; *.py) r=python3;; *) r=ruby;; esac; \\"
195-
puts " echo \"== $a\"; $r bin/design/$a > /tmp/aud-$a.txt 2>&1; tail -4 /tmp/aud-$a.txt; done"
200+
# Emitted as runner+path pairs rather than a `case` on the extension, because the extension does
201+
# not determine the runner -- see `runner_for`.
202+
# Each pair is quoted, because one of the runners is `bin/rails runner` -- two words -- and an
203+
# unquoted list would split it and try to execute `bin/rails`.
204+
pairs = selected.keys.sort.map { |n| %("#{runner_for(n)}|bin/design/#{n}") }
205+
# The exit code is printed, not just the tail. Several of these say something reassuring in prose
206+
# while exiting non-zero, and one -- `dead-code` -- exits 1 on its 147 documented findings and is
207+
# *expected* to. Reading only the last four lines of a 30-audit run is how a red check gets filed
208+
# as a green one.
209+
puts " for p in #{pairs.join(" ")}; do \\"
210+
puts " r=\"${p%%|*}\"; a=\"${p##*|}\"; o=\"/tmp/aud-$(basename \"$a\").txt\"; \\"
211+
puts " $r \"$a\" > \"$o\" 2>&1; echo \"== $a exit=$?\"; tail -4 \"$o\"; done"
196212

197213
# **Expect this to say "most of them" for anything on the render path, and do not treat that as the
198214
# tool being unhelpful.** A browser audit renders real pages, so a changed view or Stimulus

design.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,10 @@ fails on any glyph it does not recognise, which makes a one-off a decision rathe
379379
| upload a file | `bi-upload` | | ask for it again | `bi-arrow-repeat` |
380380
| invite a person | `bi-person-plus` | | promote / demote | `bi-arrow-up-circle` / `bi-arrow-down-circle` |
381381
| remove a person | `bi-person-dash` | | more actions | `bi-three-dots-vertical` |
382+
| manage people | `bi-people` | | change a person's access | `bi-person-gear` |
383+
| reset a password | `bi-key` | | compare with another period | `bi-arrow-left-right` |
384+
| a CSV | `bi-filetype-csv` | | copy to the clipboard | `bi-clipboard` |
385+
| adjust an allocation | `bi-sliders` | | total something up | `bi-calculator` |
382386
383387
**Import points in, export points out** — because import and export are movements in and out of a
384388
box, and the box is the app. This app had them the other way round for the length of the migration:
@@ -674,6 +678,14 @@ collapse.** Both halves were learned the hard way, from a column that was report
674678
675679
- **If any row in a table can reach three actions, every row in that table gets the menu.** Applied
676680
per row, one table ends up with three inline buttons on some rows and one on others.
681+
- **Or drop to two, where one of the three was a View the first cell already provides.** The rule is
682+
about reaching three, not about the menu being the only cure, and both barcode tables show the
683+
cheaper answer: `/barcode_items` and then `/admin/barcode_items` carried View, Edit and Delete
684+
inline — 349px, once the widest actions column in the app — and the View goes because the record's
685+
name in the first cell links to it. That leaves a settled pair, always both present, so they stay
686+
inline: two clicks saved over a menu, and the column cannot move because nothing in it is
687+
conditional. Prefer this where it applies; a menu holding two unconditional actions is the thing
688+
`row-actions-audit` files as an advisory.
677689
- **If *which* actions exist depends on status, role or state, use the menu whatever the count.**
678690
`/partners` chose its actions from a five-branch `case` on partner status, so reading down one
679691
screen the column measured **170, 120, 170, 241, 0 and 170px** — a different label, a different

docs/changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,7 @@ zero across these commits; several were pre-existing bugs the old markup had bee
353353
| `daba48850` | **`responsive-audit` reports zero across the whole app, and the last two findings were the same class of false positive as the flicker.** Both remaining findings were "fixed/sticky chrome covers 186px of a 360px viewport", on `/admin/base_items` and `/admin/partners`. The check sums the vertical bands of everything `fixed` or `sticky`; **a `sticky` element with `top` and `bottom` both `auto` is pinned to a horizontal edge**, and its band scrolls away with the content rather than occluding a fixed strip. The frozen actions column is precisely that — measured on `/admin/partners` at 740×360, eight `td.cell-actions` at `top: auto, bottom: auto, right: 0px` in consecutive bands (216..269, 269..322, 322..375), unioned to 186 of 360 and over the 50% threshold, for a column that eats **no height at all**. Same family as the ancestor bug fixed an hour earlier: a geometric rule reading an element whose geometry does not mean what the rule assumes. **Five controls, all correct**: a right-pinned column over eight rows scores 0, a sticky topbar 64, a fixed bottom bar 56, both together 120 (unioned, not summed), and a topbar beside a pinned column scores only the topbar's 64. Three full runs byte-identical, **0 findings across 155 routes**. **Then the interesting part, which is a correction of my own.** Having removed the column, a four-page spot check showed nothing pinned and I concluded — and wrote into the code and the to-do — that the check had gone inert on this app. So the run now prints what it considered, per the suite's own "count what you examined" rule, and that line **falsified me in one run**: it considers **30 elements across 146 page visits**, identical on three runs, every one of them `.table-rail`, the fixed 24px scroll rail on a wide table, at 24px of 360 — about 7%. Live input, negative verdict. Both claims corrected in place. What is genuinely unexercised is the *reporting* path above 50%, which no real screen reaches; the five controls live nowhere yet because the check is inline rather than exported, and `docs/todo.md` names the extraction. **Two doc corrections in the same commit.** `design-decisions.md` still said `/broadcast_announcements/1/edit` and `/partners/children/new` "still 404 and are still skipped by every audit" — fixed the next day by `3403c72b7`; marked superseded rather than rewritten, because a dated entry is a record and not a status page, and re-verified at 155 routes with an empty not-reached list. And `todo.md`'s skills figures said 17 files/1,383 lines against an actual **18/1,599**. The other two numbers in that file were re-measured and both still hold: 11 of `UiHelper`'s 27 methods live, and 9 users whose `display_name` is the placeholder. |
354354
| `3c4812e72` | **The skills-symlink premise, verified — it holds, and symlinks are followed.** The standing item to make `.claude/skills/` available outside this repo rested on a claim nobody had tested here: that Claude also reads `~/.claude/skills/`. It had never been tested because that directory has never existed on this machine, so the plan's foundation was an assumption. **Asking a model whether it can see a file is the weakest available evidence**, so it was checked with a canary: a skill at `/tmp/canary-skill/SKILL.md` carrying **two different random tokens**, one in the frontmatter `description` and one in the body, installed exactly as the plan proposes — as a symlink, `~/.claude/skills/canary-probe -> /tmp/canary-skill`. `claude -p` run from `/tmp`, a directory with no `.claude/skills` of its own, returned **both**. Two tokens because they answer different questions: the description token proves *registration*, the body token proves the file was *read*. **The negative control is what makes it evidence** — the symlink was removed while the file was left on disk inside the working directory, and the same prompt returned `NO-CANARY-SKILL`, excluding the confound that a positive result alone leaves open (the file simply being near the session's cwd). Corroborating but insufficient on its own, and ranked as such: the client binary carries **14** references to `~/.claude/skills/`, including a `synced` subdirectory named in help text — string evidence shows the path is *known*, only the canary shows it is *loaded*. Both artefacts were removed and `~/.claude/skills/` left absent, so `todo.md` still describes the machine as it stands. **The symlink itself is still not done** and still needs a decision: it writes outside the repo, and the one real tradeoff is unchanged — a symlink points at a path, so if this repo moves the skills stop working silently, while a copy trades that for two versions that drift. Generalised into `evidence-discipline` as "verify a mechanism with a canary, not with a self-report". |
355355
| `718e5cbaf` | **Ran the seven audits I had skipped, and built the thing that stops the skipping.** Yesterday's changes removed **fifty keyboard tab stops** and altered two control sizes, and the audits re-run afterwards were chosen from memory — so `keyboard-audit`, `wcag22-audit`, `row-actions-audit`, `tooltip-audit`, `wcag-audit`, `icon-audit` and `table-audit` were all skipped, every one of them a check measuring exactly what had changed. **Run now, and the tab-stop removal broke nothing**: keyboard-audit 146 screens at 1280 and again at 375, no findings; axe 160 pages 0 violations; wcag22 no failures with its examined counts printed; tooltip 653 icon-only controls 0 defects; table-audit 155 tables, 0 mixed weights, 0 filled buttons in a row. **Two audits exit non-zero and neither is from yesterday**, which is worth stating rather than implying: `icon-audit` reports `bi-arrow-left-right` ("Compare with the previous period") and `bi-key` ("Reset password") as glyphs absent from the lexicon, and `row-actions-audit` reports `/admin/barcode_items` carrying 3 inline actions with no menu, plus advisories on nine tables that need a human. **One genuine inconsistency surfaced**: `bi-envelope` now carries three labels — "Re-send invitation" on `/organization` against "Re-send invite" in `admin/users/_list` and `partners/_partner_row` — for the same action. Pre-existing (the old `title` said "Re-send invitation" too), but it took a proper `aria-label` for the audit to see it. **The fix for the skipping is `bin/design/which-audits.rb`.** Not a checklist, deliberately: a checklist is memory written down, correct the day it is written and silently wrong the day an audit is added. Instead omission is impossible by construction — each audit declares its inputs in its own header (`AUDIT-READS: RENDER`), the script **enumerates `bin/design/` itself** rather than carrying a list, and **a file that neither declares nor is named a non-audit fails the run**, so a new audit cannot drop silently out of every future selection. Six non-audits are excused with a reason each, because an unexplained absence reads identically to an oversight. Bundles are coarse on purpose: a browser audit drives real pages, so for a render-path change the honest answer is *most of them* — asked by hand about one view and one controller I picked six and the truthful number was twenty-eight. **Three controls, in the three directions the failure can hide**: a view change selects the render audits; a **docs-only change selects zero browser audits** (otherwise it is not selecting, it is saying "everything"); and a planted undeclared file fails with exit 2, proven both with the file present and after removing it. It also prints what it considered and did *not* select, and a pasteable command for the selected set. Fixed on the way: the `state.rb` paragraph in `onboarding.md` was **inside** a ```bash fence and rendering as shell. Generalised into `audit-suite` as "after a change, do not choose the audits from memory". |
356+
| `TBD` | **Both red audits cleared, the label unified, and the new selector earned its keep by finding two bugs in itself.** `icon-audit` **2 findings → 0** and `row-actions-audit` **1 → 0**, both now exit 0. **The row actions fix is the one its sibling already made.** design.md settles that a row reaching three actions gets the menu, and `/admin/barcode_items` carried View, Edit and Delete — but `/barcode_items` had the identical shape and resolved it the cheaper way, with the reasoning recorded in the partial: the View goes because the record's name in the first cell links to it, leaving a settled pair that is always both present, so they stay inline. Two clicks saved over a menu and a column that cannot move, because nothing in it is conditional. Collapsing to a menu would have satisfied the rule and left two parallel tables looking different for no reason. design.md now records **dropping to two** as an accepted resolution beside "gets the menu", since the rule is about reaching three and not about the menu being the only cure. **The two icon findings were unregistered, not wrong**: the lexicon is a meaning→glyph registry whose own note says a new glyph must be a decision rather than a reflex, so `bi-key` = *reset a password* and `bi-arrow-left-right` = *compare with another period* are now in it. **And the lexicon had already drifted from design.md** in the way its note warns against — "change both together" — with **6 entries** (`bi-people`, `bi-person-gear`, `bi-filetype-csv`, `bi-sliders`, `bi-clipboard`, `bi-calculator`) in the JSON and absent from the prose table. All 8 are in both now, checked both directions. **The label is "Re-send invitation" in all three places**, chosen because the code and the user-facing noun already say *invitation* everywhere (`invitation_status`, `reinvitable?`, `resend_user_invitation_path`) — the two `Re-send invite` call sites changed, the dated mockup left as a record. **`which-audits.rb` selected all 30 and they were all run, which is the point**: 30 clean, including axe 0 violations over 160 pages, keyboard no findings, responsive no findings, page-audit 0 defects, template-compile all compile, `dead-code` at its documented 147. `form-validation` names `/partners/family_requests/new` under "7 forms an empty submit does not exercise" — informational, not a finding, and the right discipline. **Using the tool found two defects in the tool.** It printed `ruby bin/design/dead-code.rb`, which fails: three Ruby audits need the app booted, and they are exactly the three that mention `Rails.`, so the runner is now inferred rather than listed and a new Rails-using audit gets it for free. And the pasteable command reported only each audit's last four lines — so it now prints **`exit=`** too, because several of these say something reassuring in prose while exiting non-zero and `dead-code` exits 1 by design. 81 examples 0 failures. |
356357

357358

358359

0 commit comments

Comments
 (0)