syntaxai/tdd.md · commit d83c69c

Blog post: /GIT/ URL refactor postmortem — plan vs actual

Companion to yesterday's plan post. The /goal fired, PR #42 landed
an hour later, this is the promised retrospective. Scorecard image
shows where the plan held exactly (49 references, 8 content files,
one regex, 7/7 verifier verdict), where it slightly missed (test
case count predicted ~3 actual 9, defensive over-listing of 1 test
file), and one cross-boundary reference the /goal scope didn't
enumerate explicitly (bare /<owner>/<repo> breadcrumb INSIDE a
/GIT/ page). Documents the reusable b32_<old>_url_redirect shape
for future URL refactors.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
author
syntaxai <[email protected]>
date
2026-05-25 13:48:22 +01:00
parent
684f257
commit
d83c69c5fe152e5f010e4a27a7aa6b4b85fcd891

4 files changed · +199 −0

added content/blog/sama-v2-git-url-refactor-postmortem.md +88 −0
@@ -0,0 +1,88 @@
1+# The `/GIT/` URL refactor shipped — plan vs actual
2+
3+[Yesterday's plan post](/blog/sama-v2-git-url-refactor-plan) sketched the refactor that drops the redundant `:owner` segment from `/GIT/:owner/:repo/` URLs. The `/goal` fired. [PR #42](/GIT/tdd.md/commit/df9d41a) 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.
4+
5+The headline first:
6+
7+> **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.
8+
9+The full scorecard:
10+
11+![Plan vs actual scorecard for the /GIT/ URL refactor](/images/git-url-postmortem-scorecard.png?v=1)
12+
13+## Where the plan held exactly
14+
15+Five clauses landed on-the-nose with no interpretation room:
16+
17+- **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.
18+- **8 content files** — the plan listed every markdown file by name. The merge touched exactly those 8.
19+- **One regex** — the anti-fudge clause that forbade hand-maintained URL maps held without temptation. [`rewriteOldGitUrl`](/GIT/tdd.md/blob/main/src/b32_git_url_redirect.ts) is one regex + one template literal. Total: 5 lines of pure logic in a 13-line Layer-1 helper.
20+- **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.
21+- **`LIVE_REPO_OWNER` stays exported** — the constant is still used by [`b51_render_repo.ts`](/GIT/tdd.md/blob/main/src/b51_render_repo.ts) for the "syntaxai/tdd.md" breadcrumb display text, by [`b51_render_commit.ts`](/GIT/tdd.md/blob/main/src/b51_render_commit.ts) for the commit-page repo link, and by [`c14_git.ts`](/GIT/tdd.md/blob/main/src/c14_git.ts) for repo identity. Removing it would have been a separate, larger refactor.
22+
23+## Where the plan slightly over-shot or under-shot
24+
25+Two clauses missed reality by a small margin — both in safe directions:
26+
27+**Helper test cases: predicted ~3, actual 9.**
28+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.
29+
30+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.
31+
32+**Test file churn: plan listed 2, actual 1.**
33+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.
34+
35+## What the plan didn't anticipate
36+
37+One genuine surprise the plan missed:
38+
39+**The bare `/<owner>/<repo>` breadcrumb link inside the `/GIT/` commit page.** [`b51_render_commit.ts:103`](/GIT/tdd.md/blob/main/src/b51_render_commit.ts) 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.
40+
41+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.
42+
43+## The pattern that emerged
44+
45+The reusable building block this refactor produced:
46+
47+```
48+src/
49+├── b32_<old>_url_redirect.ts ← pure transform (Layer 1)
50+├── b32_<old>_url_redirect.test.ts ← sibling test, all kinds + non-match
51+└── d21_handlers_fallback.ts ← one Layer-3 wrapper:
52+ match → 301 Response with Location
53+```
54+
55+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.
56+
57+Concrete near-future candidates:
58+
59+- If `/sama/v2/example-crud` becomes `/sama/v2/examples/crud` next month: same shape. `b32_sama_examples_url_redirect.ts` + one Layer-3 wrapper.
60+- 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.
61+- If the SAMA spec ever needs `/sama/discipline/<slug>` instead of `/sama/<slug>`: same shape.
62+
63+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.
64+
65+## What the merge looked like in the wild
66+
67+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:
68+
69+```
70+$ curl -I https://tdd.md/GIT/syntaxai/tdd.md/blob/main/src/b32_sama_v2_verify.ts
71+HTTP/2 301
72+location: /GIT/tdd.md/blob/main/src/b32_sama_v2_verify.ts
73+cache-control: public, max-age=86400
74+
75+$ curl -L https://tdd.md/GIT/syntaxai/tdd.md/blob/main/src/b32_sama_v2_verify.ts
76+HTTP/2 200
77+< file content >
78+```
79+
80+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.
81+
82+## What lands next
83+
84+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.
85+
86+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.
87+
88+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.
added public/images/git-url-postmortem-scorecard.png +0 −0
added public/images/git-url-postmortem-scorecard.svg +105 −0
@@ -0,0 +1,105 @@
1+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 700" width="1200" height="700">
2+ <rect width="1200" height="700" fill="#0a0a0a"/>
3+
4+ <!-- Header -->
5+ <g font-family="ui-monospace, 'SF Mono', 'JetBrains Mono', Menlo, Consolas, monospace">
6+ <text x="80" y="46" font-size="20" font-weight="600" fill="#909090">Plan vs actual — /GIT/ URL refactor postmortem</text>
7+ <text x="80" y="92" font-size="32" font-weight="700" fill="#e8e8e8">The plan held. 19 files, one regex, zero regressions.</text>
8+ <text x="80" y="120" font-size="14" fill="#7a7a7a">PR #42 · commit df9d41a · 153 insertions, 69 deletions · 9 new helper tests · 7/7 ✓ before and after</text>
9+ </g>
10+
11+ <!-- Column headers -->
12+ <g font-family="ui-monospace, 'SF Mono', 'JetBrains Mono', Menlo, Consolas, monospace" font-size="13" font-weight="600" letter-spacing="2">
13+ <text x="100" y="172" fill="#909090">CLAUSE</text>
14+ <text x="600" y="172" fill="#909090">PREDICTED</text>
15+ <text x="820" y="172" fill="#909090">ACTUAL</text>
16+ <text x="1020" y="172" fill="#909090">VERDICT</text>
17+ </g>
18+ <line x1="80" y1="184" x2="1120" y2="184" stroke="#2a2a2a" stroke-width="1"/>
19+
20+ <!-- Rows -->
21+ <g font-family="ui-monospace, 'SF Mono', 'JetBrains Mono', Menlo, Consolas, monospace" font-size="15">
22+
23+ <!-- row 1: source files -->
24+ <text x="100" y="216" fill="#c8c8c8">source files touched</text>
25+ <text x="600" y="216" fill="#8a8a8a">~10 + helper + test</text>
26+ <text x="820" y="216" fill="#c8c8c8">11 (incl. b32 + test)</text>
27+ <text x="1020" y="216" fill="#7ec77e">✓ on target</text>
28+
29+ <!-- row 2: content files -->
30+ <text x="100" y="246" fill="#c8c8c8">content files rewritten</text>
31+ <text x="600" y="246" fill="#8a8a8a">8 (plan listed each)</text>
32+ <text x="820" y="246" fill="#c8c8c8">8</text>
33+ <text x="1020" y="246" fill="#7ec77e">✓ exact</text>
34+
35+ <!-- row 3: references -->
36+ <text x="100" y="276" fill="#c8c8c8">/GIT/ references absorbed</text>
37+ <text x="600" y="276" fill="#8a8a8a">49</text>
38+ <text x="820" y="276" fill="#c8c8c8">49</text>
39+ <text x="1020" y="276" fill="#7ec77e">✓ exact</text>
40+
41+ <!-- row 4: regex -->
42+ <text x="100" y="306" fill="#c8c8c8">redirect mechanism</text>
43+ <text x="600" y="306" fill="#8a8a8a">one regex</text>
44+ <text x="820" y="306" fill="#c8c8c8">one regex</text>
45+ <text x="1020" y="306" fill="#7ec77e">✓ anti-fudge held</text>
46+
47+ <!-- row 5: helper LOC -->
48+ <text x="100" y="336" fill="#c8c8c8">Layer 1 helper size</text>
49+ <text x="600" y="336" fill="#8a8a8a">~5 lines</text>
50+ <text x="820" y="336" fill="#c8c8c8">13 lines</text>
51+ <text x="1020" y="336" fill="#7ec77e">✓ in band</text>
52+
53+ <!-- row 6: helper test cases -->
54+ <text x="100" y="366" fill="#c8c8c8">helper test cases</text>
55+ <text x="600" y="366" fill="#8a8a8a">~3</text>
56+ <text x="820" y="366" fill="#c8c8c8">9</text>
57+ <text x="1020" y="366" fill="#c89a3a">~ over-covered (3×)</text>
58+
59+ <!-- row 7: test count -->
60+ <text x="100" y="396" fill="#c8c8c8">total test count</text>
61+ <text x="600" y="396" fill="#8a8a8a">379 → 379+</text>
62+ <text x="820" y="396" fill="#c8c8c8">379 → 388 (+9)</text>
63+ <text x="1020" y="396" fill="#7ec77e">✓ green</text>
64+
65+ <!-- row 8: verifier -->
66+ <text x="100" y="426" fill="#c8c8c8">/sama/v2/verify</text>
67+ <text x="600" y="426" fill="#8a8a8a">7/7 ✓ before + after</text>
68+ <text x="820" y="426" fill="#c8c8c8">7/7 ✓ before + after</text>
69+ <text x="1020" y="426" fill="#7ec77e">✓ verdict held</text>
70+
71+ <!-- row 9: LIVE_REPO_OWNER -->
72+ <text x="100" y="456" fill="#c8c8c8">LIVE_REPO_OWNER constant</text>
73+ <text x="600" y="456" fill="#8a8a8a">stays exported</text>
74+ <text x="820" y="456" fill="#c8c8c8">stays exported</text>
75+ <text x="1020" y="456" fill="#7ec77e">✓ no scope creep</text>
76+
77+ <!-- row 10: git protocol URLs -->
78+ <text x="100" y="486" fill="#c8c8c8">git protocol URLs</text>
79+ <text x="600" y="486" fill="#8a8a8a">untouched (scope-out)</text>
80+ <text x="820" y="486" fill="#c8c8c8">untouched</text>
81+ <text x="1020" y="486" fill="#7ec77e">✓ no breakage</text>
82+
83+ <!-- row 11: alias mode -->
84+ <text x="100" y="516" fill="#c8c8c8">alias mode (both URLs work)</text>
85+ <text x="600" y="516" fill="#8a8a8a">forbidden — 301 only</text>
86+ <text x="820" y="516" fill="#c8c8c8">301 only</text>
87+ <text x="1020" y="516" fill="#7ec77e">✓ converges</text>
88+
89+ <!-- row 12: surprises -->
90+ <text x="100" y="546" fill="#c8c8c8">unanticipated surprises</text>
91+ <text x="600" y="546" fill="#8a8a8a">(unknown)</text>
92+ <text x="820" y="546" fill="#c8c8c8">1 (commit-test churn)</text>
93+ <text x="1020" y="546" fill="#c89a3a">~ minor scope nit</text>
94+ </g>
95+
96+ <!-- Footer: takeaway -->
97+ <rect x="80" y="582" width="1040" height="74" fill="#0f1a0f" stroke="#1f3f1f" stroke-width="1" rx="6"/>
98+ <g font-family="ui-monospace, 'SF Mono', 'JetBrains Mono', Menlo, Consolas, monospace">
99+ <text x="100" y="610" font-size="16" font-weight="600" fill="#7ec77e">The reusable shape:</text>
100+ <text x="100" y="634" font-size="14" fill="#c8c8c8">b32_&lt;old&gt;_url_redirect.ts (pure transform, ~5–13 lines) + sibling test + one Layer-3 wrapper in the fallback handler</text>
101+ </g>
102+
103+ <!-- Watermark -->
104+ <text x="1120" y="684" text-anchor="end" font-family="ui-monospace, 'SF Mono', 'JetBrains Mono', Menlo, Consolas, monospace" font-size="12" fill="#5a5a5a">https://tdd.md</text>
105+</svg>
modified src/a31_blog.ts +6 −0
@@ -12,6 +12,12 @@ export interface BlogEntry {
1212 }
1313
1414 export const ALL_POSTS: BlogEntry[] = [
15+ {
16+ slug: "sama-v2-git-url-refactor-postmortem",
17+ title: "The /GIT/ URL refactor shipped — plan vs actual",
18+ description: "Yesterday's /goal post planned the refactor that drops the redundant :owner segment from /GIT/:owner/:repo/ URLs. The /goal fired, PR #42 landed an hour later, here's the promised postmortem. Headline: 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. Walks the scorecard: where the plan held exactly (49 references — exact grep match; 8 content files — exact match; one regex anti-fudge held; LIVE_REPO_OWNER kept exported as designed), where it slightly missed (helper test cases predicted ~3, actual 9 — over-coverage 3× because the natural coverage was URL-surface-by-kind not regex-by-pattern; test file churn predicted 2, actual 1 — the commit-test file had no URL strings, defensive over-listing in the /goal is cheap), and what the plan didn't anticipate (the bare /<owner>/<repo> breadcrumb link INSIDE a /GIT/ commit page that points OUTSIDE the /GIT/ surface — a cross-boundary reference the /goal scope didn't enumerate explicitly; the right call at code-edit time was to leave it alone, but next plan should enumerate cross-references between in-scope and out-of-scope surfaces). The reusable shape: b32_<old>_url_redirect.ts (pure 5-line transform) + sibling test + one Layer-3 wrapper in the fallback handler. The cost flattens — first URL refactor under SAMA v2 needed a plan post + postmortem + evening of work; next one needs the helper + sed pass + maybe an hour. Concrete future candidates listed: /sama/v2/example-crud → /sama/v2/examples/crud, /blog/<slug> → /blog/<yyyy-mm>/<slug>, /sama/<slug> → /sama/discipline/<slug>. The interesting next data point: whether the SECOND URL refactor confirms cost-flattening. Either outcome is informative.",
19+ date: "2026-05-25",
20+ },
1521 {
1622 slug: "sama-v2-git-url-refactor-plan",
1723 title: "Shortening /GIT/ URLs: a single-tenant URL has a redundant segment",