HANDOFF: permissions

Only project permissions is authoritative. Ignore all archived records for other projects. Updates 1–5 have been applied; superseded choices are discarded. No later update or question is available.

CURRENT SETTINGS
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
Implement evaluate(payload) returning:
{allowed: boolean, reason: string, matched: [grant_index], audit: [reason]}

Subject fields: tenant, roles[], groups[], owner, suspended.
Resource fields: tenant, id.
Payload includes subject, resource, now, issued_at, action, grants, role_parents.
Inputs are ordinary ASCII. Do not mutate inputs.

EVALUATION ORDER
1. Different subject/resource tenants: deny, reason "tenant".
2. now - issued_at > 100: deny, reason "token".
   Token age exactly 100 is valid.
3. Suspended subject: deny, reason "suspended".
4. Owner subject: allow, reason "owner".
All these early returns have matched=[].
Tenant, token and suspension checks take precedence over owner bypass.

ROLES
Start with subject.roles union subject.groups.
Follow role_parents transitively to include inherited roles.
Tolerate cycles; do not mutate the role lists or parent mapping.

GRANT MATCHING
A grant matches only when all conditions hold:
- Its role is in the expanded roles.
- Its action equals the requested action, ASCII case-insensitively, OR its action is "*".
- Its scope equals resource.id exactly; prefix matching is disabled.
- Its expires is null, OR now <= expires.
Expiration equality is valid.

Gather EVERY matching grant's original index, retaining original grant order. Do not stop after finding an allow or deny.

DECISION
- No matching grants: deny, reason "default", matched=[].
- Matching grants: reason "grants"; allow only if at least one matching allow exists and no matching deny exists.
- Deny-only matches deny.
- A matching deny overrides matching allows.
- Return all matching indices even when the result is denied.

AUDIT
audit_denials is false, so audit=[] for every result, including early denials.
Fixed conditional contract if later changed: audit=[reason] only for denied results when audit_denials is true; otherwise [].

UNRESOLVED / CONTINUATION
No unresolved setting values remain among the twelve configuration keys.
The supplied contract does not explicitly name the grant field representing allow/deny; preserve that semantic requirement without inventing a field name.
Implementation language and additional validation/error behavior are not specified.
Future target-project updates replace only their named keys and leave all others unchanged.