Estimate: 25m · Depends on: 6.8.1
The read half: every key-addressed surface resolves old keys, from ONE central path.
Central resolution: projectsService.resolveByKey — live identifier first, then the alias table; returns the project + a viaAlias flag (callers decide serve-vs-redirect). The /api/projects/[key] route family switches to it and serves on an alias hit (the verified REST behaviour — REST calls on the old key just work; the response DTO carries the canonical identifier).
Issue pages: /issues/[key] + /issues/[key]/edit — on a lookup miss, parse the identifier prefix (split on the first hyphen; keys are 3–5 alnum, never hyphenated), resolve via alias, recompose the canonical identifier, and redirect() with 308 to the canonical URL (the verified link behaviour: old links redirect, their text is never rewritten). Neither live nor alias → the existing 404. Active-project pinning is id-based and unaffected (note + assert).
/issues/PROD-7 and /issues/PROD-7/edit 308-redirect to their NIF-7 canonicals; an identifier that was NEVER live 404s; a RELEASED alias's URLs 404 (release breaks old links — the mirror rule)./api/projects/PROD/... route serves identically to its NIF twin (spot-assert access, members, estimation-config), with the canonical key in the DTO.resolveByKey); chained renames PROD→NIF→ZAP resolve BOTH old keys flat (no chain-walking: each alias row maps directly to the project).resolveByKey substrate)app/(authed)/issues/[key]/page.tsx + edit/page.tsx (the lookup-miss seam); lib/projects/index.ts (getActiveProject — id-pinned, unaffected)/api/projects/[key] route family (access / members / estimation-config)