Policy after Update 6: audit_denials = false and default_allow = false. All other settings stay as in the handoff note.

The handoff left the grant schema unspecified, so I made these assumptions:
- Grant fields are `role`, `action`, `scope` and `expires`.
- Allow versus deny is read from `effect`, with `"deny"` meaning deny and anything else meaning allow. An `allow` boolean is also honored if present.
- Prefix scope means `resource.id` starts with `grant.scope`.
- `wildcard_action` is false, so a grant action of `"*"` matches only when the requested action is literally `"*"`. That falls out of plain equality.

The input is never mutated.

{"files":{"main.ts":"import {run} from './core.ts';\nimport {POLICY} from './policy.ts';\nexport function evaluate(payload: any): any { return run(payload, POLICY); }\n","core.ts":"import {isAllow, scopeMatches, actionMatches, expiryOk} from './helpers.ts';\n\nexport function run(payload: any, policy: any): any {\n  const subject = payload.subject || {};\n  const resource = payload.resource || {};\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 (payload.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 && subject.owner) return fin(true, 'owner', []);\n  let list: string[] = (subject.roles || []).slice();\n  if (policy.group_union) list = list.concat(subject.groups || []);\n  const roles = new Set<string>(list);\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      for (const p of ps) {\n        if (!roles.has(p)) { roles.add(p); stack.push(p); }\n      }\n    }\n  }\n  const grants = payload.grants || [];\n  const matched: number[] = [];\n  let anyAllow = false;\n  let anyDeny = false;\n  grants.forEach((g: any, i: number) => {\n    if (!roles.has(g.role)) return;\n    if (!actionMatches(g.action, payload.action, policy)) return;\n    if (!scopeMatches(g.scope, resource.id, policy.resource_scope)) return;\n    if (!expiryOk(g.expires, payload.now, policy.expiry_inclusive)) return;\n    matched.push(i);\n    if (isAllow(g)) anyAllow = true; else anyDeny = true;\n  });\n  if (matched.length === 0) return fin(!!policy.default_allow, 'default', []);\n  const allowed = policy.deny_wins ? (anyAllow && !anyDeny) : anyAllow;\n  return fin(allowed, 'grants', matched);\n}\n","policy.ts":"export const POLICY: any = {\n  deny_wins: false,\n  expiry_inclusive: false,\n  inherit_roles: false,\n  owner_bypass: false,\n  action_casefold: false,\n  group_union: false,\n  resource_scope: 'prefix',\n  suspended_denies: false,\n  token_ttl: 200,\n  wildcard_action: false,\n  audit_denials: false,\n  default_allow: false,\n};\n","helpers.ts":"export function isAllow(g: any): boolean {\n  if (g.effect !== undefined) return String(g.effect).toLowerCase() !== 'deny';\n  if (g.allow !== undefined) return !!g.allow;\n  return true;\n}\n\nexport function scopeMatches(scope: any, id: any, mode: string): boolean {\n  if (mode === 'exact') return scope === id;\n  return typeof scope === 'string' && typeof id === 'string' && id.startsWith(scope);\n}\n\nexport function actionMatches(ga: any, action: any, policy: any): boolean {\n  if (policy.wildcard_action && ga === '*') return true;\n  if (policy.action_casefold) {\n    return String(ga).toLowerCase() === String(action).toLowerCase();\n  }\n  return ga === action;\n}\n\nexport function expiryOk(expires: any, now: number, inclusive: boolean): boolean {\n  if (expires === null || expires === undefined) return true;\n  return inclusive ? now <= expires : now < expires;\n}\n"}}