syntaxai/tdd.md · commit 642e917

Move /sama/<discipline> → /sama/discipline/<slug> — hypothesis test of cost-flattening

Second instance of the b32_<old>_url_redirect pattern. Same shape
as PR #42's git-url-drop-owner: pure Layer-1 transform + Layer-3
Response wrapper + sed pass on content refs + Bun route change +
sitemap handler update.

- src/b32_sama_discipline_url_redirect.ts (13 lines, fixed enum of
  the four spec-frozen discipline slugs)
- src/b32_sama_discipline_url_redirect.test.ts (12 cases)
- src/d21_handlers_fallback.ts: 301 redirect block placed adjacent
  to the existing rewriteOldGitUrl block (same pattern, twice now)
- src/d21_app.ts: route /sama/:slug → /sama/discipline/:slug;
  sitemap handler ALL_SAMA mapping → /sama/discipline/<slug>
- src/b32_edit_resolve.ts: pageUrl for sama disciplines now emits
  the new shape; /sama/skill and /sama/v2 unaffected
- Sed pass: 13 files in content/ + src/ with /sama/<discipline> URL
  refs rewritten to /sama/discipline/<discipline>

Tests: 419/419 pass (+12 new redirect-helper cases).

Co-Authored-By: Claude Opus 4.7 <[email protected]>
author
syntaxai <[email protected]>
date
2026-05-25 15:54:10 +01:00
parent
c35c5f7
commit
642e917f8f318d3eeab008f059ef93c876e697b6

17 files changed · +116 −23

modified content/blog/agentic-coding-corpus-three-patterns.md +1 −1
@@ -106,7 +106,7 @@ The prescribed solution:
106106
107107 > *"Lock them in a box (can ONLY modify explicitly assigned files), Make them work in phases (CONTRACT → STUB → TEST → IMPLEMENT) with validation gates they cannot skip."*
108108
109-Read that solution against [SAMA's atomic property](/sama/atomic) (one responsibility per module, ~700-line split) and the [iron law](/sama/skill) (failing test first, verify before next phase). DUMBAI's CONTRACT → STUB → TEST → IMPLEMENT is the iron-law cycle expressed in agent-task vocabulary; "lock them to assigned files" is *Atomic* + *Sorted* enforced as a sandbox boundary.
109+Read that solution against [SAMA's atomic property](/sama/discipline/atomic) (one responsibility per module, ~700-line split) and the [iron law](/sama/skill) (failing test first, verify before next phase). DUMBAI's CONTRACT → STUB → TEST → IMPLEMENT is the iron-law cycle expressed in agent-task vocabulary; "lock them to assigned files" is *Atomic* + *Sorted* enforced as a sandbox boundary.
110110
111111 **The top comment on `1rug14a`** lands at the same answer from a different angle:
112112
modified content/blog/sama-empirical-modeled-green.md +2 −2
@@ -137,7 +137,7 @@ The blog post is the receipt; the URL is the proof.
137137
138138 - Live dogfood: <https://tdd.md/sama/verify?repo=syntaxai/tdd.md>
139139 - The four checks documented:
140- [Sorted](/sama/sorted) · [Architecture](/sama/architecture) ·
141- [Modeled](/sama/modeled) · [Atomic](/sama/atomic)
140+ [Sorted](/sama/discipline/sorted) · [Architecture](/sama/discipline/architecture) ·
141+ [Modeled](/sama/discipline/modeled) · [Atomic](/sama/discipline/atomic)
142142 - Previous post in this series:
143143 [When the verifier said "split this"](/blog/sama-empirical-c21-split)
modified content/blog/sama-v2-on-ramp-gap.md +1 −1
@@ -25,7 +25,7 @@ Specifically:
2525
2626 - **The `/goal` workflow rule** lives at `/var/home/scri/.claude/projects/-var-home-scri-Documents-tdd-md/memory/feedback_goal_authoring_workflow.md` — an agent-private memory file. A human contributor literally cannot read it. The rule that determines how every PR on this site is authored is preserved for the agent and invisible to everyone else.
2727
28-- **The layer-prefix convention** (`a31_*` = Layer 0, `b32_*` = Layer 1, `c14_*` = Layer 2, `d21_*` = Layer 3) is partially documented in [/sama/v2 §1.1](/sama/v2#11-layers), partially in the `/sama/sorted` discipline page, and partially encoded in the source code itself. A new contributor has to triangulate.
28+- **The layer-prefix convention** (`a31_*` = Layer 0, `b32_*` = Layer 1, `c14_*` = Layer 2, `d21_*` = Layer 3) is partially documented in [/sama/v2 §1.1](/sama/v2#11-layers), partially in the `/sama/discipline/sorted` discipline page, and partially encoded in the source code itself. A new contributor has to triangulate.
2929
3030 - **The branch-PR-deploy flow** (branch → `gh pr create` → merge → push p620 → `flatpak-spawn --host scripts/p620/deploy-tdd-md.sh` → live-verify) is in *zero* blog posts as a standalone procedure. It's mentioned as a constraint inside individual `/goal` bodies, embedded in PR commit messages, scattered across the recent /goals archive. Never collected.
3131
modified content/home.md +4 −4
@@ -20,10 +20,10 @@ The five §5 core metrics — **graphDepth · fanByLayer · boundaryRatio · wor
2020
2121 ## The four pillars
2222
23-- **[S — Sorted.](/sama/sorted)** Lexicographic file order equals import direction. The dependency graph is the file tree.
24-- **[A — Architecture.](/sama/architecture)** Every file's prefix maps to one layer with explicit allowed/forbidden contents. No rogue files.
25-- **[M — Modeled.](/sama/modeled)** Every behavior file has a sibling test. Every external input is parsed at the boundary, never cast.
26-- **[A — Atomic.](/sama/atomic)** Files cap at ~700 lines. Split per domain, never via barrel re-exports.
23+- **[S — Sorted.](/sama/discipline/sorted)** Lexicographic file order equals import direction. The dependency graph is the file tree.
24+- **[A — Architecture.](/sama/discipline/architecture)** Every file's prefix maps to one layer with explicit allowed/forbidden contents. No rogue files.
25+- **[M — Modeled.](/sama/discipline/modeled)** Every behavior file has a sibling test. Every external input is parsed at the boundary, never cast.
26+- **[A — Atomic.](/sama/discipline/atomic)** Files cap at ~700 lines. Split per domain, never via barrel re-exports.
2727
2828 ## SAMA in your agent-coding stack
2929
modified content/sama/architecture.md +1 −1
@@ -72,4 +72,4 @@ When you spot a violation, the fix is always the same shape: split the file alon
7272
7373 ---
7474
75-[← S — Sorted](/sama/sorted) · [/sama](/sama) · [next: M — Modeled →](/sama/modeled)
75+[← S — Sorted](/sama/discipline/sorted) · [/sama](/sama) · [next: M — Modeled →](/sama/discipline/modeled)
modified content/sama/atomic.md +1 −1
@@ -97,4 +97,4 @@ It also keeps **token cost** bounded. An agent given "edit `c51_render_reports.t
9797
9898 ---
9999
100-[← M — Modeled](/sama/modeled) · [/sama](/sama) · [back to the four properties](/sama)
100+[← M — Modeled](/sama/discipline/modeled) · [/sama](/sama) · [back to the four properties](/sama)
modified content/sama/modeled.md +1 −1
@@ -102,4 +102,4 @@ The fix: move the type to `c31_project_config.ts`, write a `parseProjectConfig`
102102
103103 ---
104104
105-[← A — Architecture](/sama/architecture) · [/sama](/sama) · [next: A — Atomic →](/sama/atomic)
105+[← A — Architecture](/sama/discipline/architecture) · [/sama](/sama) · [next: A — Atomic →](/sama/discipline/atomic)
modified content/sama/sorted.md +1 −1
@@ -73,4 +73,4 @@ A file tree where:
7373
7474 ---
7575
76-[← /sama](/sama) · [next: A — Architecture →](/sama/architecture)
76+[← /sama](/sama) · [next: A — Architecture →](/sama/discipline/architecture)
modified src/a31_blog.ts +1 −1
@@ -15,7 +15,7 @@ export const ALL_POSTS: BlogEntry[] = [
1515 {
1616 slug: "sama-v2-on-ramp-gap",
1717 title: "Every artifact has a URL. The on-ramp doesn't.",
18- description: "Open tdd.md in a fresh browser. Count the entry points — /sama/v2 (spec), /sama/v2/verify (live), /blog (narrative), /goals (contracts), /GIT/tdd.md (source), /sitemap.xml (every URL). Six artifacts, each auditable, each its own blog post subject. Now ask the question the site exists to answer for a reader: 'how do I contribute?' There is no answer. No CONTRIBUTING.md, no /contributing URL, no canonical on-ramp anywhere. Forty PRs of preaching auditability — and the most basic on-ramp test failing the entire time. This post is the drama. Walks the artifact-vs-path table: every OUTPUT lives in a canonical place (✓), every authoring rule lives somewhere a contributor wouldn't think to look (the /goal workflow rule is in an agent-private memory file that humans literally cannot read; the layer convention is in /sama/v2 §1.1 + /sama/sorted + the source tree; the branch-PR-deploy flow is scattered across PR commit messages and individual /goal bodies; the image convention lives in memory only; the Containerfile gotcha lives in one PR's commit message). Concrete 10-step thought experiment of what a new contributor adding their first blog post would actually have to do — ends in 'give up, open an issue saying how do I contribute'. Frames this as a SAMA v2 self-violation parallel to /blog/sama-v2-goal-chain-gap one level up: that post fixed the /goal in the chain; this post argues the workflow itself is the next missing artifact, same structural failure applied to procedure instead of contract. Sketches the fix: one CONTRIBUTING.md, two surfaces (./CONTRIBUTING.md at repo root + /contributing on the site, same markdown source), with three load-bearing properties — single source of truth (links don't restate), dogfooded (its own creation is /goal-driven at /goals/contributing-md), drift-detectable (a test that grep-checks CONTRIBUTING.md contains only references not restatements). The /goal that drives the implementation follows this post per the plan-execute-postmortem pattern.",
18+ description: "Open tdd.md in a fresh browser. Count the entry points — /sama/v2 (spec), /sama/v2/verify (live), /blog (narrative), /goals (contracts), /GIT/tdd.md (source), /sitemap.xml (every URL). Six artifacts, each auditable, each its own blog post subject. Now ask the question the site exists to answer for a reader: 'how do I contribute?' There is no answer. No CONTRIBUTING.md, no /contributing URL, no canonical on-ramp anywhere. Forty PRs of preaching auditability — and the most basic on-ramp test failing the entire time. This post is the drama. Walks the artifact-vs-path table: every OUTPUT lives in a canonical place (✓), every authoring rule lives somewhere a contributor wouldn't think to look (the /goal workflow rule is in an agent-private memory file that humans literally cannot read; the layer convention is in /sama/v2 §1.1 + /sama/discipline/sorted + the source tree; the branch-PR-deploy flow is scattered across PR commit messages and individual /goal bodies; the image convention lives in memory only; the Containerfile gotcha lives in one PR's commit message). Concrete 10-step thought experiment of what a new contributor adding their first blog post would actually have to do — ends in 'give up, open an issue saying how do I contribute'. Frames this as a SAMA v2 self-violation parallel to /blog/sama-v2-goal-chain-gap one level up: that post fixed the /goal in the chain; this post argues the workflow itself is the next missing artifact, same structural failure applied to procedure instead of contract. Sketches the fix: one CONTRIBUTING.md, two surfaces (./CONTRIBUTING.md at repo root + /contributing on the site, same markdown source), with three load-bearing properties — single source of truth (links don't restate), dogfooded (its own creation is /goal-driven at /goals/contributing-md), drift-detectable (a test that grep-checks CONTRIBUTING.md contains only references not restatements). The /goal that drives the implementation follows this post per the plan-execute-postmortem pattern.",
1919 date: "2026-05-25",
2020 },
2121 {
modified src/b32_edit_resolve.test.ts +1 −1
@@ -4,7 +4,7 @@ import { resolveEdit } from "./b32_edit_resolve.ts";
44 test("resolves an existing sama page", () => {
55 const r = resolveEdit("sama", "sorted");
66 expect(r).not.toBeNull();
7- expect(r?.pageUrl).toBe("/sama/sorted");
7+ expect(r?.pageUrl).toBe("/sama/discipline/sorted");
88 expect(r?.filePath).toBe("content/sama/sorted.md");
99 expect(r?.title).toMatch(/Sorted/);
1010 });
modified src/b32_edit_resolve.ts +7 −1
@@ -58,10 +58,16 @@ export const resolveEdit = (section: string, slug: string): ResolvedEdit | null
5858 if (!SAFE_SLUG.test(slug)) return null;
5959 const title = lookupTitle(section, slug);
6060 if (title === null) return null;
61+ // /sama discipline pages live under /sama/discipline/<slug> as of
62+ // PR #53; other sections keep the /<section>/<slug> shape.
63+ const pageUrl =
64+ section === "sama" && slug !== "skill" && slug !== "v2"
65+ ? `/sama/discipline/${slug}`
66+ : `/${section}/${slug}`;
6167 return {
6268 section,
6369 slug,
64- pageUrl: `/${section}/${slug}`,
70+ pageUrl,
6571 filePath: `content/${section}/${slug}.md`,
6672 title,
6773 };
added src/b32_sama_discipline_url_redirect.test.ts +55 −0
@@ -0,0 +1,55 @@
1+import { describe, expect, test } from "bun:test";
2+import { rewriteOldSamaDisciplineUrl } from "./b32_sama_discipline_url_redirect.ts";
3+
4+describe("rewriteOldSamaDisciplineUrl", () => {
5+ test("rewrites /sama/sorted", () => {
6+ expect(rewriteOldSamaDisciplineUrl("/sama/sorted")).toBe(
7+ "/sama/discipline/sorted",
8+ );
9+ });
10+
11+ test("rewrites /sama/architecture", () => {
12+ expect(rewriteOldSamaDisciplineUrl("/sama/architecture")).toBe(
13+ "/sama/discipline/architecture",
14+ );
15+ });
16+
17+ test("rewrites /sama/modeled", () => {
18+ expect(rewriteOldSamaDisciplineUrl("/sama/modeled")).toBe(
19+ "/sama/discipline/modeled",
20+ );
21+ });
22+
23+ test("rewrites /sama/atomic", () => {
24+ expect(rewriteOldSamaDisciplineUrl("/sama/atomic")).toBe(
25+ "/sama/discipline/atomic",
26+ );
27+ });
28+
29+ test("returns null for new-form URLs", () => {
30+ expect(rewriteOldSamaDisciplineUrl("/sama/discipline/sorted")).toBe(null);
31+ });
32+
33+ test("returns null for /sama landing", () => {
34+ expect(rewriteOldSamaDisciplineUrl("/sama")).toBe(null);
35+ });
36+
37+ test("returns null for /sama/v2 and its subpaths", () => {
38+ expect(rewriteOldSamaDisciplineUrl("/sama/v2")).toBe(null);
39+ expect(rewriteOldSamaDisciplineUrl("/sama/v2/verify")).toBe(null);
40+ });
41+
42+ test("returns null for /sama/skill", () => {
43+ expect(rewriteOldSamaDisciplineUrl("/sama/skill")).toBe(null);
44+ });
45+
46+ test("returns null for invented discipline slugs", () => {
47+ expect(rewriteOldSamaDisciplineUrl("/sama/syntropic")).toBe(null);
48+ });
49+
50+ test("returns null for non-/sama paths", () => {
51+ expect(rewriteOldSamaDisciplineUrl("/blog/foo")).toBe(null);
52+ expect(rewriteOldSamaDisciplineUrl("/")).toBe(null);
53+ expect(rewriteOldSamaDisciplineUrl("")).toBe(null);
54+ });
55+});
added src/b32_sama_discipline_url_redirect.ts +15 −0
@@ -0,0 +1,15 @@
1+// b32 — Layer 1 pure helper: rewrite the legacy /sama/<discipline>
2+// URL shape to /sama/discipline/<discipline>. Second instance of the
3+// b32_<old>_url_redirect pattern established by b32_git_url_redirect.ts
4+// — same shape, same sibling-test structure, same Layer-3 wrapper
5+// in d21_handlers_fallback. The four discipline slugs are spec-frozen
6+// (§0 SAMA = Sorted, Architecture, Modeled, Atomic), so the regex
7+// hard-codes the enumeration rather than accepting any kebab slug.
8+
9+const OLD_PATTERN = /^\/sama\/(sorted|architecture|modeled|atomic)$/;
10+
11+export const rewriteOldSamaDisciplineUrl = (pathname: string): string | null => {
12+ const m = OLD_PATTERN.exec(pathname);
13+ if (m === null) return null;
14+ return `/sama/discipline/${m[1]}`;
15+};
modified src/b51_render_docs_layout.ts +1 −1
@@ -18,7 +18,7 @@ import {
1818 } from "./b51_render_layout.ts";
1919
2020 export interface DocsPageOptions extends Omit<PageOptions, "bodyHtml"> {
21- // The route path the user is on, e.g. "/sama/sorted". Used to
21+ // The route path the user is on, e.g. "/sama/discipline/sorted". Used to
2222 // compute prev/next.
2323 pathForDocs: string;
2424 // Optional override of which file the "edit on GitHub" link
modified src/d21_app.ts +2 −2
@@ -202,7 +202,7 @@ export const createApp = (port: number) => Bun.serve({
202202 lastmod: p.date,
203203 }));
204204 const samaUrls: SitemapUrl[] = ALL_SAMA.map((d) => ({
205- loc: `${SITE_BASE_URL}/sama/${d.slug}`,
205+ loc: `${SITE_BASE_URL}/sama/discipline/${d.slug}`,
206206 }));
207207 const guideUrls: SitemapUrl[] = ALL_GUIDES.map((g) => ({
208208 loc: `${SITE_BASE_URL}/guides/${g.slug}`,
@@ -415,7 +415,7 @@ ${rows}
415415
416416 "/sama": samaLandingHandler,
417417
418- "/sama/:slug": samaSlugHandler,
418+ "/sama/discipline/:slug": samaSlugHandler,
419419
420420 "/goals": goalsLandingHandler,
421421
modified src/d21_handlers_fallback.ts +17 −0
@@ -23,6 +23,7 @@ import {
2323 repoBrowseHandler,
2424 } from "./d21_handlers_repo_browse.ts";
2525 import { rewriteOldGitUrl } from "./b32_git_url_redirect.ts";
26+import { rewriteOldSamaDisciplineUrl } from "./b32_sama_discipline_url_redirect.ts";
2627
2728 const isGitProtocol = (pathname: string, search: URLSearchParams): boolean => {
2829 if (pathname.includes(".git/") || pathname.endsWith(".git")) return true;
@@ -138,6 +139,22 @@ export const appFetch = async (req: Request): Promise<Response> => {
138139 });
139140 }
140141
142+ // Legacy /sama/<discipline> URLs permanent-redirect to the new
143+ // /sama/discipline/<slug> namespace. Same pattern-as-redirect shape
144+ // as the /GIT/ block above — pure Layer-1 transform + Layer-3
145+ // Response wrapper. Hypothesis-test instance of the pattern's
146+ // reusability (see /blog/sama-v2-git-url-refactor-postmortem).
147+ const newSamaPath = rewriteOldSamaDisciplineUrl(url.pathname);
148+ if (newSamaPath !== null) {
149+ return new Response(null, {
150+ status: 301,
151+ headers: {
152+ Location: newSamaPath,
153+ "Cache-Control": "public, max-age=86400",
154+ },
155+ });
156+ }
157+
141158 // SAMA-native repo browse at /GIT/:repo/{tree,blob,raw}/:ref/<path>.
142159 // The wildcard path needs more flexibility than Bun's :param routes
143160 // give us (no slashes), so we match in the fallback fetch instead.
modified src/d21_handlers_sama.ts +5 −5
@@ -431,10 +431,10 @@ The four discipline pages below are the v1 practitioner-facing version of SAMA.
431431 ### reading order
432432
433433 If you're new to this:
434-1. Start with **[Sorted](/sama/sorted)** — it has the verification grep that everything else is built around.
435-2. Then **[Architecture](/sama/architecture)** — what each layer prefix means.
436-3. Then **[Modeled](/sama/modeled)** — where types and tests live.
437-4. Then **[Atomic](/sama/atomic)** — the split rule that keeps the rest honest as the codebase grows.
434+1. Start with **[Sorted](/sama/discipline/sorted)** — it has the verification grep that everything else is built around.
435+2. Then **[Architecture](/sama/discipline/architecture)** — what each layer prefix means.
436+3. Then **[Modeled](/sama/discipline/modeled)** — where types and tests live.
437+4. Then **[Atomic](/sama/discipline/atomic)** — the split rule that keeps the rest honest as the codebase grows.
438438
439439 Each page is short, opinionated, and ends with the common mistakes you'll see if the discipline lapses.
440440
@@ -539,7 +539,7 @@ export const samaSlugHandler = async (req: { params: { slug: string } }): Promis
539539 }
540540 const rawMd = await file.text();
541541 // Stripe-style "older version" banner — prepended to every v1
542- // discipline page so a reader landing directly on /sama/sorted etc.
542+ // discipline page so a reader landing directly on /sama/discipline/sorted etc.
543543 // sees the v2-spec pointer before the v1 prose.
544544 const v1Banner = `> **You are reading the v1 practitioner version of this property.** The formal, normative v2 specification — frozen core, profile mechanism, deterministic verifier, and §5 cross-repo measurement chain — lives at **[/sama/v2](/sama/v2)**. This page is preserved as background reading.\n\n`;
545545 const md = v1Banner + rawMd;