- Shipped
- August 14, 2026 at 10:34 PM UTC
- Author
- kamo
- Commit
- 6d4a0d0
The count moved 58 -> 52 -> 47 -> 57 because every version hard-coded a list of gate-helper names and the codebase has more than anyone can recall. Six were missed across three passes — denyUnless, hasAnyRight, sessionHasRight, forbidden, findByMemberAndRight, checkRole — and each miss reported a working permission as dead. audit_v3.py names none of them. It uses the structural difference between code that USES a right and prose that DISCUSSES one: argument position (hasRight(request, RoleRightType.X), List.of(RoleRightType.X)) or receiver position (RoleRightType.X.name(), which is how OrganizationController gates branding). A javadoc paragraph mentioning {@code MANAGE_TIMECARDS} no longer reads as a check — that was the source of most phantom 'enforced' results. It validates against 13 rights confirmed by reading the code and exits non-zero rather than emit a list it disagrees with. The guard caught two defects while the analyzer was being written, and disproved one of my own earlier corrections: VIEW_DASHBOARD is decorative. The file-level pass had rescued it wrongly. Net vs the shipped list: +13 (six sales-agent rights, four marketing VIEW_*, VIEW_DASHBOARD, VIEW_TIME_OFF, VIEW_VOICEMAIL — all rescued by file-level matching but never passed to anything), -2 (VIEW_PAYROLL, MANAGE_PAYROLL, which genuinely are enforced). Analyzer and its output are committed so the number is reproducible instead of asserted, and the file says not to hand-edit — editing one entry by eye is how it drifted before.