1. One refusal, in words, with a code
Every module's refusal now comes from one function. It answers with a sentence, a code and what was needed:
You don't have access to do this in Finance. Ask a workspace owner or admin.
code: 'forbidden',need: 'finance:journal.post'
The sentence is for the person. The code is for the screen, which
should never read a sentence to decide what to do. The
need is for whoever looks at the log, or for an admin
who wants to grant exactly that.
2. A refusal is not always a failure
One of our screens asks for "timesheets waiting for your approval". Someone who isn't an approver gets a 403 there. A new version of the screen showed them "Couldn't load the timesheets waiting for you" with a Retry button that could never work.
A 403 on that read doesn't mean the read failed. It means "you aren't an approver", and the right thing to show is nothing. We caught it in review. The rule we took from it: before showing "couldn't load" with Retry, ask whether trying again could ever work for this person.
3. A permission that gates nothing is a false claim
Our role editor listed all 246 permissions as checkboxes. We wrote a scan that reads the routes and handlers and marks each permission as enforced or not. 67 of them gated nothing: ticking the box changed nothing anywhere.
A checkbox that does nothing tells an admin they've restricted something they haven't. The editor now offers only permissions that something enforces, and a test fails and prints the list if an unenforced one is ever offered again. Permissions already saved in custom roles are left alone.
What we found on the way
Code that parsed the old sentence. One feature decided what to do by matching the refusal's words, so changing the words would have broken it without any error. That is why the code exists: sentences change, codes don't.
ADLR is at https://adlr.app: $5 per user a month, every module included, 30 days free with no card.