I made some interpretation choices where the spec is ambiguous. Nothing was run or tested.

- **Grant shape:** I assumed each grant has `role`, `action`, `scope`, `expires` and `effect`, where `effect` is "allow" or "deny". A missing `effect` is treated as allow.
- **Action wildcard:** I treat a grant action of `*` as matching any requested action.
- **Prefix scope:** `resource_scope` is "exact" under the current policy. I kept a `startsWith` branch for prefix scope.
- **Early returns:** The tenant, token, suspended and owner checks return `matched=[]`. Denied early returns also carry the audit entry.
- **Default case:** With no matching grants, the result is `default_allow` with reason `default`.
- **Owner check:** The owner is identified by `subject.owner` being truthy. The spec is ambiguous, so I also accept `subject.owner === resource.id` or a `resource.owner` equal to the subject's id.

```json
{"files":{"main.ts":"import {run} from './core.ts';\nimport {POLICY} from './policy.ts';\nexport function evaluate(payload: any): any { return run(payload, POLICY); }\n","policy.ts":"export const POLICY: any = {\"action_casefold\": true, \"audit_denials\": true, \"default_allow\": true, \"deny_wins\": true, \"expiry_inclusive\": true, \"group_union\": true, \"inherit_roles\": true, \"owner_bypass\": true, \"resource_scope\": \"exact\", \"suspended_denies\": true, \"token_ttl\": 100, \"wildcard_action\": true};\n","helpers.ts":"export function lower(s: any): string { return String(s).replace(/[A-Z]/g, (c) => c.toLowerCase()); }\n","core.ts":"import {lower} from './helpers.ts';\n\nexport function run(payload: any, policy: any): any {\n  const subject = payload.subject || {};\n  const resource = payload.resource || {};\n  const now = payload.now;\n  const fin = (allowed: boolean, reason: string, matched: number[]) => ({\n    allowed,\n    reason,\n    matched,\n    audit: !allowed && policy.audit_denials ? [reason] : [],\n  });\n  if (subject.tenant !== resource.tenant) return fin(false, 'tenant', []);\n  if (now - payload.issued_at > policy.token_ttl) return fin(false, 'token', []);\n  if (policy.suspended_denies && subject.suspended) return fin(false, 'suspended', []);\n  if (policy.owner_bypass) {\n    const isOwner = subject.owner === true || (subject.owner !== undefined && subject.owner !== null && subject.owner !== false && (subject.owner === resource.id || subject.owner === resource.owner));\n    if (isOwner) return fin(true, 'owner', []);\n  }\n  const roles = new Set<string>(subject.roles || []);\n  if (policy.group_union) for (const g of subject.groups || []) roles.add(g);\n  if (policy.inherit_roles) {\n    const parents = payload.role_parents || {};\n    const stack = Array.from(roles);\n    while (stack.length) {\n      const r = stack.pop() as string;\n      const ps = parents[r];\n      if (!ps) continue;\n      const list = Array.isArray(ps) ? ps : [ps];\n      for (const p of list) {\n        if (!roles.has(p)) {\n          roles.add(p);\n          stack.push(p);\n        }\n      }\n    }\n  }\n  const grants = payload.grants || [];\n  const matched: number[] = [];\n  let anyAllow = false;\n  let anyDeny = false;\n  const act = policy.action_casefold ? lower(payload.action) : payload.action;\n  for (let i = 0; i < grants.length; i++) {\n    const g = grants[i];\n    if (!roles.has(g.role)) continue;\n    const ga = policy.action_casefold ? lower(g.action) : g.action;\n    const actOk = ga === act || (policy.wildcard_action && g.action === '*');\n    if (!actOk) continue;\n    const scope = g.scope;\n    const scopeOk = policy.resource_scope === 'prefix'\n      ? String(resource.id).startsWith(String(scope))\n      : scope === resource.id;\n    if (!scopeOk) continue;\n    const e = g.expires;\n    const expOk = e === null || e === undefined || (policy.expiry_inclusive ? now <= e : now < e);\n    if (!expOk) continue;\n    matched.push(i);\n    if (g.effect === 'deny') anyDeny = true;\n    else anyAllow = true;\n  }\n  if (matched.length === 0) {\n    return fin(!!policy.default_allow, 'default', []);\n  }\n  const allowed = policy.deny_wins ? anyAllow && !anyDeny : anyAllow;\n  return fin(allowed, 'grants', matched);\n}\n"}}
```