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.