TARGET PROJECT: permissions — handoff note

Current configuration (final values only; superseded and unrelated-project settings discarded):
{
  "deny_wins": true,
  "expiry_inclusive": true,
  "inherit_roles": true,
  "owner_bypass": true,
  "action_casefold": true,
  "group_union": true,
  "resource_scope": "exact",
  "suspended_denies": true,
  "token_ttl": 100,
  "wildcard_action": true,
  "audit_denials": false,
  "default_allow": false
}

Fixed 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. Do not mutate inputs.

Algorithm / exceptions, in order:
1. If subject and resource tenants differ: deny, reason "tenant", matched=[].
2. Else if now - issued_at > token_ttl (100): deny, reason "token", matched=[].
3. Else if suspended_denies (true) and subject suspended: deny, reason "suspended", matched=[].
4. Else if owner_bypass (true) and subject is owner: allow, reason "owner", matched=[].
5. Initial roles = subject.roles plus subject.groups, because group_union=true. If inherit_roles=true, expand roles transitively via role_parents, tolerating cycles.
6. A grant matches when: its role is in the expanded roles; its action equals the requested action ASCII case-insensitively (action_casefold=true), or grant action "*" matches when wildcard_action=true; its scope equals resource.id (resource_scope="exact"); and it is unexpired — expires null, or now <= expires because expiry_inclusive=true.
7. matched = original indices of every matching grant, gathered in order.
8. If no matches: allowed = default_allow (false), reason "default".
9. If matches exist: reason "grants". Because deny_wins=true, allow only if at least one allow grant and no deny grant; otherwise deny.
10. audit = [reason] only on denied results when audit_denials=true; since audit_denials=false, audit is always [] under current choices. Early-return denies follow this same audit rule; allowed results have audit [].

Unresolved items: none known. All 12 configuration keys above have a current target-project value.