The /GIT/ URL refactor shipped — plan vs actual

Yesterday's plan post sketched the refactor that drops the redundant :owner segment from /GIT/:owner/:repo/ URLs. The /goal fired. PR #42 landed an hour later. This post is the promised companion postmortem: where the plan held, where it didn't, and what the merge produced that the plan couldn't predict.

The headline first:

The plan held. 19 files changed (153 insertions, 69 deletions), one regex absorbs every old URL, 9 new helper tests, 388/388 green, 7/7 ✓ on /sama/v2/verify before and after the merge.

The full scorecard:

Plan vs actual scorecard for the /GIT/ URL refactor

#Where the plan held exactly

Five clauses landed on-the-nose with no interpretation room:

  • 49 references — the grep count in the plan was exactly the number of substitutions the merge produced. Not a single new file slipped in between plan-write and merge that the regex would have missed.
  • 8 content files — the plan listed every markdown file by name. The merge touched exactly those 8.
  • One regex — the anti-fudge clause that forbade hand-maintained URL maps held without temptation. rewriteOldGitUrl is one regex + one template literal. Total: 5 lines of pure logic in a 13-line Layer-1 helper.
  • 7/7 ✓ before, 7/7 ✓ after — the verifier verdict was constant across the merge. The §4 check logic didn't need touching; the structural choices the refactor wanted to make were already conformant.
  • LIVE_REPO_OWNER stays exported — the constant is still used by b51_render_repo.ts for the "syntaxai/tdd.md" breadcrumb display text, by b51_render_commit.ts for the commit-page repo link, and by c14_git.ts for repo identity. Removing it would have been a separate, larger refactor.

#Where the plan slightly over-shot or under-shot

Two clauses missed reality by a small margin — both in safe directions:

Helper test cases: predicted ~3, actual 9. The plan said "new helper test (~3 cases) covers the redirect regex". Once the helper existed and the test file was open, three felt anaemic — the natural coverage was all four URL kinds (tree/blob/raw/commit) plus the .diff variant plus three explicit non-match cases (already-new URL, other-org URL, non-/GIT/ path) plus the boundary case (/GIT/syntaxai/tdd.md without a trailing segment). That's 9 cases. Over-coverage by 3×, no harm done — Bun ran them in 2ms.

The interesting question this raises: why was the plan's estimate low? Because the plan was thinking about the regex (which has one pattern, hence ~3 cases would suffice), but the test ended up thinking about the URL surface (which has four kinds × two outcomes = 8 natural categories). The right unit of test coverage is the user-facing surface, not the internal pattern. A small lesson for next time's plan: estimate test coverage by surface area, not by code complexity.

Test file churn: plan listed 2, actual 1. The /goal listed both b51_render_repo.test.ts and b51_render_commit.test.ts as files needing test-string updates. The first did — one comment line referenced the old URL shape. The second turned out to have no URL strings at all; it only pins the export shape via typeof renderCommitView === "function". So the plan over-listed by one file. Not a bug — defensive enumeration is cheap — but a data point for how /goal-authoring tends to err: over-list and let reality narrow.

#What the plan didn't anticipate

One genuine surprise the plan missed:

The bare /<owner>/<repo> breadcrumb link inside the /GIT/ commit page. b51_render_commit.ts:103 emits a breadcrumb link from the commit page back to the bare-repo view at /syntaxai/tdd.md. This link is inside a /GIT/ page but points outside the /GIT/ URL surface. The plan declared "bare-repo view is out of scope" but didn't call out this specific cross-boundary link.

The right call (made at code-edit time) was to leave it alone — it points at /syntaxai/tdd.md, which is the explicit scope-out from the /goal. But a stricter /goal would have called out cross-boundary references explicitly. For next plan: when scoping a refactor in/out by URL prefix, also enumerate cross-references between the in-scope and out-of-scope surfaces. They're the places where the seams show.

#The pattern that emerged

The reusable building block this refactor produced:

src/
├── b32_<old>_url_redirect.ts        ← pure transform (Layer 1)
├── b32_<old>_url_redirect.test.ts   ← sibling test, all kinds + non-match
└── d21_handlers_fallback.ts         ← one Layer-3 wrapper:
                                        match → 301 Response with Location

Five lines of pure logic + ~10 lines of wrapper + sibling test. Every future URL refactor that needs "old URL form is permanent-redirect to new URL form" reaches for this shape.

Concrete near-future candidates:

  • If /sama/v2/example-crud becomes /sama/v2/examples/crud next month: same shape. b32_sama_examples_url_redirect.ts + one Layer-3 wrapper.
  • If /blog/<slug> ever gains a date prefix /blog/<yyyy-mm>/<slug>: same shape. The new slug is computed from ALL_POSTS[slug].date, the helper transforms old → new.
  • If the SAMA spec ever needs /sama/discipline/<slug> instead of /sama/<slug>: same shape.

The cost-per-refactor flattens: the first URL refactor under SAMA v2 needed a plan post, a postmortem post, ~10 source-file changes, a sed pass, and an evening of work. The next URL refactor needs a 5-line helper, a sibling test, one Layer-3 wrapper, a sed pass, and maybe an hour. The plan post can be shorter because the pattern is now public; the postmortem post can be skipped entirely if nothing surprises.

#What the merge looked like in the wild

The 301 redirect handler caught its first real legacy URL within minutes of the deploy — every internal /GIT/syntaxai/tdd.md/... reference that I'd typed by muscle memory during the implementation phase (yes, while editing the very same files) got rewritten by the regex before reaching the browse handler. The smoke test was satisfying:

$ curl -I https://tdd.md/GIT/syntaxai/tdd.md/blob/main/src/b32_sama_v2_verify.ts
HTTP/2 301
location: /GIT/tdd.md/blob/main/src/b32_sama_v2_verify.ts
cache-control: public, max-age=86400

$ curl -L https://tdd.md/GIT/syntaxai/tdd.md/blob/main/src/b32_sama_v2_verify.ts
HTTP/2 200
< file content >

A user clicking an old /GIT/syntaxai/tdd.md/... link from a Google search result, a Twitter card cache, or someone else's blog post lands on the new URL with the file content intact. Cache TTL is 86400 — search engines will refetch within a day and update their index.

#What lands next

The pattern-as-redirect shape is on file. The empirical chain post-baseline is intact (the n=8 cross-repo measurements still point at the same source files, just at shorter URLs). The /sama/v2/verify page continues to make the same structural claim it did yesterday, against the same source tree, at the same 7/7 verdict — just from URLs nine characters shorter.

The interesting next thing is whether the second URL refactor — when it happens — confirms the cost-flattening hypothesis. If /sama/v2/example-crud migrates next month and the implementation takes an hour, that's the data point. If it takes another evening, the pattern wasn't as portable as it looked. Either result is informative.

Until then: 19 files, one regex, zero regressions. The /goal workflow + SAMA v2 discipline + the verifier as merge gate produces this kind of boring, exactly-as-planned outcome with surprising regularity. The boring outcomes are the load-bearing ones.