Skip to content

Commit 1f11681

Browse files
chore(plans): close out slug-history-redirect (PR #92)
Closeout: flip to done, tick all validations (11 new redirect tests + 255/255 API tests, lint + type-check clean), fill Notes (MergeApply threading, buzz live-index undelivery, tag handle-scan, deliberate 5-min Cache-Control) + Follow-ups (periodic sweeper, buzz live-index when writers land, CDN edge-cache concerns). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent ac8e9c7 commit 1f11681

1 file changed

Lines changed: 20 additions & 10 deletions

File tree

plans/slug-history-redirect.md

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
---
2-
status: in-progress
2+
status: done
33
depends: []
44
specs:
55
- specs/behaviors/slug-handles.md
66
issues: [80]
7+
pr: 92
78
---
89

910
# Plan: SlugHistory 90-day redirect handler
@@ -117,13 +118,13 @@ The plugin uses `fastify.addHook('onRequest', ...)` so it fires for every reques
117118

118119
## Validation
119120

120-
- [ ] `InMemoryState.slugHistory` populated at boot from non-expired records; expired entries skipped.
121-
- [ ] All three write services call `stateApply.upsertSlugHistory` after the gitsheets upsert.
122-
- [ ] Fastify plugin registered after `services`, before `static-web`.
123-
- [ ] All 9 test cases above pass.
124-
- [ ] Existing 244 API tests still pass.
125-
- [ ] `npm run type-check && npm run lint` clean.
126-
- [ ] Spec compliance: GET `/<entity>/<old-slug>` with a non-expired SlugHistory → 301; live wins; multi-hop chain follows; expired → no redirect.
121+
- [x] `InMemoryState.slugHistory` populated at boot from non-expired records; expired entries skipped via `indexSlugHistory`'s `expiresAt < now` guard.
122+
- [x] All three write services call `stateApply.upsertSlugHistory` after the gitsheets upsert (`project.write.ts`, `person.write.ts`, `account-claim.ts`'s `MergeApply.replay`).
123+
- [x] Fastify `slug-redirect` plugin registered after `services`, before `static-web`.
124+
- [x] 11 test cases pass — single-hop project + person renames, sub-route preservation, query-string preservation, multi-hop A→B→C, live-wins, expired-skip, reserved-segment passthrough, tag rename, `/api/*` never intercepted, key-format determinism.
125+
- [x] All 255 API tests pass (244 pre-existing + 11 new).
126+
- [x] `npm run type-check && npm run lint` clean.
127+
- [x] Spec compliance: GET `/<entity>/<old-slug>` with a non-expired SlugHistory → 301; live wins; multi-hop chain follows; expired → no redirect.
127128

128129
## Risks / unknowns
129130

@@ -135,8 +136,17 @@ The plugin uses `fastify.addHook('onRequest', ...)` so it fires for every reques
135136

136137
## Notes
137138

138-
*(filled at done time)*
139+
Shipped over five commits — plan opening + three implementation steps (in-memory state, write-service wiring, Fastify plugin) + tests.
140+
141+
Surprises:
142+
143+
- **Account-claim's MergeApply needed slug-history threading.** Project + person renames live in their own write services where the StateApply is directly accessible, so wiring `upsertSlugHistory` was a one-line addition. Account-claim uses a `MergeApply` wrapper that batches all the post-onboarding rewrites for later replay onto the route-level StateApply — slug-history needed to ride along through that wrapper, which meant adding it to `MergeApplyInput` + `MergeApply.replay`.
144+
- **Buzz live-index is intentionally fake.** Buzz slugs are keyed by `${projectId}:${buzzSlug}` in the live index (`buzzByProjectAndSlug`); we don't have the projectId cheaply at the URL-pattern level (only the project's slug). For now the buzz pattern always returns `false` from `liveIndex` — which means a slug-history hit always wins for buzz, regardless of whether the buzz slug is live under a different project. No writer currently creates buzz slug-history records, so this is hypothetical.
145+
- **Tag live-check scans `tagIdByHandle.keys()`.** Tags are uniquely keyed by `(namespace, slug)` but slug-history's key is `tag:<slug>` (no namespace). The live-check walks the handle map looking for any namespace that owns the slug. Tag slug-history has no live writer today either; the conservative live-wins behavior matches the spec.
146+
- **The 5-min `Cache-Control` is a deliberate undersizing.** 301s are normally aggressively cached by browsers. Because our redirects can expire when the 90-day SlugHistory TTL hits, we keep cache headers short so stale-redirect windows after expiry are bounded to ~5 minutes.
139147

140148
## Follow-ups
141149

142-
*(filled at done time)*
150+
- **Periodic sweeper to purge expired SlugHistory records from the sheet.** The read path is already defensive — expired records are filtered at index time — so this is purely about keeping the on-disk sheet from growing forever. *Tracked as*: file a new issue when sheet bloat becomes measurable; today's volume is negligible.
151+
- **Buzz live-index** — when buzz renames become real, add a `buzzByGlobalSlug` map (or accept the conservative "always redirect on slug-history hit"). *Deferred* until buzz rename writers exist.
152+
- **CDN/edge cache awareness** — if/when we put the site behind a CDN, 301s would benefit from explicit cache-key handling so the redirect doesn't outlive its TTL at the edge. *Deferred* until cutover plans introduce a CDN.

0 commit comments

Comments
 (0)