HANDOFF NOTE: project "permissions" (current state after Update 5)

SCOPE
Only the "permissions" project is authoritative. All "Archived work record" entries (archive-north/south/east/west-NN) belong to other projects and were discarded. None of their values apply here.

INTERFACE
evaluate(payload) -> {allowed: boolean, reason: string, matched: [grant_index], audit: [reason]}
- Subject: tenant, roles[], groups[], owner, suspended.
- Resource: tenant, id.
- Payload: now, issued_at, action, grants, role_parents.
- Inputs are ordinary ASCII. The payload must not be mutated.

CURRENT CONFIG (after Updates 1-5, latest value wins per key)
- deny_wins = false
- expiry_inclusive = false
- inherit_roles = false
- owner_bypass = false
- action_casefold = false (Update 2 set true, Update 5 reverted it)
- group_union = false (Update 2 set true, Update 5 reverted it)
- resource_scope = "prefix" (Update 2 set exact, Update 4 set prefix)
- suspended_denies = false (Update 2 set true, Update 4 reverted it)
- token_ttl = 200
- wildcard_action = false
- default_allow = true
- audit_denials = true

All 12 keys have a value. No key is unresolved.

ALGORITHM (in order)
1. If subject.tenant != resource.tenant: deny, reason "tenant", matched=[].
2. If now - issued_at > token_ttl (200): deny, reason "token", matched=[].
3. suspended_denies is false, so a suspended subject is not denied at this step. The subject proceeds to grant evaluation.
4. owner_bypass is false, so the owner shortcut is disabled. Owners go through normal grant evaluation.
5. Roles = subject.roles only (group_union false, so groups are ignored). inherit_roles is false, so role_parents is not followed. Cycle tolerance is moot but would be required if inheritance were enabled.
6. A grant matches when all of these hold:
   - Its role is in roles.
   - Its action equals the requested action exactly (case-sensitive). The '*' wildcard is NOT honored.
   - Its scope is a prefix of resource.id (resource_scope "prefix"). Treat a scope that equals the id as a prefix match.
   - Its expires is null, or now < expires (strict, since expiry_inclusive is false).
7. matched = original indices of all matching grants.
8. If there are no matches: allowed = default_allow (true), reason "default".
9. If there are matches: reason "grants". Because deny_wins is false, allowed is true if any matching grant allows. Otherwise it is false.
10. audit = [reason] only when the result is denied (audit_denials true). Otherwise audit = [].
    - Early denials (tenant, token) are denied results, so they get audit = [reason].
    - Allowed results get audit = [].

NOTES / ASSUMPTIONS TO CONFIRM
- The grant's allow/deny field name and its representation were not specified. Confirm the grant schema, for example an "effect" field.
- The token check is strict (> token_ttl). A difference exactly equal to 200 is still valid.
- Behavior for a prefix scope that is an empty string is unspecified. It would naturally match everything.
- No further updates have been received. The next update replaces earlier values per key and leaves other keys unchanged.