Drop redundant :owner segment from /GIT/ URLs — single regex 301
Single-tenant is already enforced by isAllowedRepo() — the owner is always "syntaxai" or the handler 404s. The URL segment was policy overhead, not data. Now: before: /GIT/syntaxai/tdd.md/blob/main/src/b32_sama_v2_verify.ts after: /GIT/tdd.md/blob/main/src/b32_sama_v2_verify.ts Inbound links survive via one regex 301 redirect in the fallback handler — the new Layer 1 helper b32_git_url_redirect.ts owns the pure pathname transform, the fallback wraps it in a Response. Sibling test covers all four URL kinds (tree/blob/raw/commit), .diff variant, and the non-match paths (other-org, already-new, non-/GIT/). Changes: - Layer 1: b32_git_url_redirect.ts (new) — rewriteOldGitUrl(pathname). - Layer 3 handlers: repoBrowseHandler + commitViewHandler drop owner from their signatures; isAllowedRepo collapses to (repo) — owner is implicit (LIVE_REPO_OWNER, still exported for c14_git + Forgejo proxy callers). - Bun route in d21_app.ts: /GIT/:owner/:repo/commit/:sha → /GIT/:repo/commit/:sha. - Fallback handler: 301 redirect block placed BEFORE gitBrowseMatch so legacy URLs never reach the browse handler; gitBrowseMatch regex drops the owner capture. - Link builders: 8 sites in b51_render_repo.ts, 2 sites in b51_render_commit.ts (the bare /<owner>/<repo> breadcrumb stays — bare-repo view is out of scope), 1 site in b51_render_edit.ts. - Content rewrite: 7 blog + spec markdown files + 1 embedded markdown in d21_handlers_sama.ts. Plan post keeps its BEFORE/AFTER example + curl docs literal (they document the redirect itself). LIVE_REPO_OWNER stays exported in a31_site_config.ts — used by the renderer call sites for the "syntaxai/tdd.md" breadcrumb header, and by c14_git operations that talk to the bare repo. Tests: 388/388 pass (379 → 388, +9 redirect-helper cases). Git protocol URLs (/syntaxai/tdd.md.git, /<owner>/<repo> bare repo view) are unchanged — agents and humans have those copy-pasted into clone commands and CI configs, scope-out per /goal. Co-Authored-By: Claude Opus 4.7 <[email protected]>
19 files changed · +153 −69
content/blog/sama-v2-git-url-refactor-plan.md
+2
−2
| @@ -15,7 +15,7 @@ Nine characters shorter. The change is small but the workflow it sits inside is | ||
| 15 | 15 | |
| 16 | 16 | ## Why dropping the owner is safe |
| 17 | 17 | |
| 18 | -The relevant code is twenty-one lines down [`src/d21_handlers_repo_browse.ts`](/GIT/syntaxai/tdd.md/blob/main/src/d21_handlers_repo_browse.ts): | |
| 18 | +The relevant code is twenty-one lines down [`src/d21_handlers_repo_browse.ts`](/GIT/tdd.md/blob/main/src/d21_handlers_repo_browse.ts): | |
| 19 | 19 | |
| 20 | 20 | ```ts |
| 21 | 21 | const isAllowedRepo = (owner: string, repo: string): boolean => |
| @@ -77,7 +77,7 @@ The plan deliberately doesn't do these things, even though each is locally appea | ||
| 77 | 77 | |
| 78 | 78 | - **No hand-maintained list of redirects.** One regex pattern covers all 49 current references and every future one. If the regex grows into "a list", the anti-fudge clause has been violated. |
| 79 | 79 | - **No removal of `LIVE_REPO_OWNER`.** The constant has callers beyond URL construction (the live-reports view, the Forgejo proxy hostname). Removing it from `a31_site_config.ts` would be a different, larger refactor that the URL change shouldn't drag in. |
| 80 | -- **No touching of git-protocol URLs.** `/syntaxai/tdd.md.git` and the bare-repo view at `/syntaxai/tdd.md` go through `isGitProtocol` + `repoMatch` in [`d21_handlers_fallback.ts`](/GIT/syntaxai/tdd.md/blob/main/src/d21_handlers_fallback.ts). Those URLs are git-client-facing — agents and humans have copy-pasted them into clone commands, into CI configs, into other agents' system prompts. Changing them risks breakage for cosmetics. They stay. | |
| 80 | +- **No touching of git-protocol URLs.** `/syntaxai/tdd.md.git` and the bare-repo view at `/syntaxai/tdd.md` go through `isGitProtocol` + `repoMatch` in [`d21_handlers_fallback.ts`](/GIT/tdd.md/blob/main/src/d21_handlers_fallback.ts). Those URLs are git-client-facing — agents and humans have copy-pasted them into clone commands, into CI configs, into other agents' system prompts. Changing them risks breakage for cosmetics. They stay. | |
| 81 | 81 | - **No alias.** Both URL forms working forever creates two canonical URLs and lets the old one quietly remain in new code. The 301 is what forces consolidation — search engines update, internal code paths rewrite themselves, and a year from now the old form is just a redirect line in one file. |
| 82 | 82 | - **No verifier change.** `/sama/v2/verify` stays at 7/7 ✓ across the merge. The §4 check logic is frozen; if a structural choice the refactor wants to make would fail the verifier, the choice changes — not the verifier. |
| 83 | 83 | |
content/blog/sama-v2-go-project-dive.md
+3
−3
| @@ -153,7 +153,7 @@ Derives from Law. No file's declared layer is contradicted by what it imports. | ||
| 153 | 153 | |
| 154 | 154 | **The `workingSetFit` is the metric I most expected to land near tdd.md's 80%** — two engineered codebases, both with linters and conventions. The measurement says otherwise. |
| 155 | 155 | |
| 156 | -**Hand-trace** (auditable per [/sama/v2 §0](/sama/v2)): running `find /tmp/dive -name '*.go' -not -name '*_test.go' -not -path '*/.git/*' -not -path '*/vendor/*' | wc -l` returns **92 source .go files**. Of those, **48** fall in [50, 500] LOC inclusive (matching `WORKING_SET_MIN_LOC` and `WORKING_SET_MAX_LOC` in [`src/a31_sama_v2.ts`](/GIT/syntaxai/tdd.md/blob/main/src/a31_sama_v2.ts)). 48 ÷ 92 = 0.5217 ≈ 52.17%. The polyglot §5 emitter at [`scripts/measure-working-set.ts`](/GIT/syntaxai/tdd.md/blob/main/scripts/measure-working-set.ts) produces the same number from the same source tree. | |
| 156 | +**Hand-trace** (auditable per [/sama/v2 §0](/sama/v2)): running `find /tmp/dive -name '*.go' -not -name '*_test.go' -not -path '*/.git/*' -not -path '*/vendor/*' | wc -l` returns **92 source .go files**. Of those, **48** fall in [50, 500] LOC inclusive (matching `WORKING_SET_MIN_LOC` and `WORKING_SET_MAX_LOC` in [`src/a31_sama_v2.ts`](/GIT/tdd.md/blob/main/src/a31_sama_v2.ts)). 48 ÷ 92 = 0.5217 ≈ 52.17%. The polyglot §5 emitter at [`scripts/measure-working-set.ts`](/GIT/tdd.md/blob/main/scripts/measure-working-set.ts) produces the same number from the same source tree. | |
| 157 | 157 | |
| 158 | 158 | The distribution explains it: **44 files under 50 LOC** (mostly small type-only modules, single-helper files, and platform-shim stubs like `dive/image/docker/docker_host_windows.go` at 6 LOC), **48 in band**, and — strikingly — **0 over 500 LOC**. `dive`'s working-set miss is not god-classes (the §4.5 Atomic check passes outright); it's the *opposite* failure mode: many files small enough to fall below the substantive-module threshold. |
| 159 | 159 | |
| @@ -161,13 +161,13 @@ The original ~80% estimate was wrong, and wrong in a direction casual eyeballing | ||
| 161 | 161 | |
| 162 | 162 | ### graphDepth, measured: 12 (originally estimated ~5) |
| 163 | 163 | |
| 164 | -The polyglot graphDepth emitter at [`scripts/measure-graph-depth.ts`](/GIT/syntaxai/tdd.md/blob/main/scripts/measure-graph-depth.ts) walks `dive`'s [`go.mod`](https://github.com/wagoodman/dive/blob/d6c691947f8fda635c952a17ee3b7555379d58f0/go.mod), collects every `.go` file's imports, filters to intra-module imports (those starting with `github.com/wagoodman/dive/`), aggregates them per-package-directory, and computes the longest path. The result for `dive@d6c69194`: **27 package directories, 80 internal edges, longest dependency chain of depth 12**. | |
| 164 | +The polyglot graphDepth emitter at [`scripts/measure-graph-depth.ts`](/GIT/tdd.md/blob/main/scripts/measure-graph-depth.ts) walks `dive`'s [`go.mod`](https://github.com/wagoodman/dive/blob/d6c691947f8fda635c952a17ee3b7555379d58f0/go.mod), collects every `.go` file's imports, filters to intra-module imports (those starting with `github.com/wagoodman/dive/`), aggregates them per-package-directory, and computes the longest path. The result for `dive@d6c69194`: **27 package directories, 80 internal edges, longest dependency chain of depth 12**. | |
| 165 | 165 | |
| 166 | 166 | A 12-deep import chain is more than twice the audit's eyeball estimate of ~5. The estimate was wrong because I was thinking in *top-level package categories* (`cmd`, `command`, `ui`, `dive`, `filetree`, `internal/utils` — six things), but the actual Go package graph treats each subdirectory as its own package. `cmd/dive/cli/internal/ui/v1/viewmodel` is a different package from `cmd/dive/cli/internal/ui/v1/view`, even though they read like one category to a human; the import graph sees them as distinct hops. The 12-deep chain weaves through subdirectories the human-readable description folded into one bullet. |
| 167 | 167 | |
| 168 | 168 | This is the same shape of finding as the workingSetFit one above: the *metric* sees the structure; the *eye* sees the categories. Both are useful, but only the metric is mechanically comparable across repos. |
| 169 | 169 | |
| 170 | -Module-granularity note: the polyglot graphDepth metric counts at the Go package-directory level — multiple `.go` files in one directory share their package and therefore their imports. This is the natural Go analog to the TS file-level metric (TS one module ≈ one file; Go one package ≈ one directory). The semantic is documented in [`src/b32_graph_depth_polyglot.ts`](/GIT/syntaxai/tdd.md/blob/main/src/b32_graph_depth_polyglot.ts). | |
| 170 | +Module-granularity note: the polyglot graphDepth metric counts at the Go package-directory level — multiple `.go` files in one directory share their package and therefore their imports. This is the natural Go analog to the TS file-level metric (TS one module ≈ one file; Go one package ≈ one directory). The semantic is documented in [`src/b32_graph_depth_polyglot.ts`](/GIT/tdd.md/blob/main/src/b32_graph_depth_polyglot.ts). | |
| 171 | 171 | |
| 172 | 172 | ## What `dive` would look like at 7/7 — the last 30% |
| 173 | 173 | |
content/blog/sama-v2-metrics-emitter.md
+3
−3
| @@ -116,7 +116,7 @@ verifier can say "0 violations" while the metric says "30% of | ||
| 116 | 116 | boundaries are in Layer 1" — contradictory pictures of the same code. |
| 117 | 117 | |
| 118 | 118 | So the detector now lives in one place — `findParseBoundaryCallSites` |
| 119 | -in [`src/a31_sama_v2.ts`](/GIT/syntaxai/tdd.md/blob/main/src/a31_sama_v2.ts) | |
| 119 | +in [`src/a31_sama_v2.ts`](/GIT/tdd.md/blob/main/src/a31_sama_v2.ts) | |
| 120 | 120 | (Layer 0, pure). The verifier consumes it. The metric consumes it. |
| 121 | 121 | They share the regex, the comment/string-literal stripping, the file |
| 122 | 122 | iteration. The two cannot diverge — if a future commit changes what |
| @@ -139,10 +139,10 @@ I picked `boundaryRatio`. A raw grep of `src/*.ts` (non-test) for | ||
| 139 | 139 | `JSON.parse(` and `new URL(` returns eleven hits. Four of them are |
| 140 | 140 | inside comments or string literals — *"`JSON.parse()` constructors |
| 141 | 141 | must not appear in..."* in the docstring of |
| 142 | -[`c14_request_parse.ts`](/GIT/syntaxai/tdd.md/blob/main/src/c14_request_parse.ts), | |
| 142 | +[`c14_request_parse.ts`](/GIT/tdd.md/blob/main/src/c14_request_parse.ts), | |
| 143 | 143 | *"parsing as `JSON.parse(` of arbitrary input..."* in the comment |
| 144 | 144 | header of |
| 145 | -[`b32_sama_v2_verify.ts`](/GIT/syntaxai/tdd.md/blob/main/src/b32_sama_v2_verify.ts). | |
| 145 | +[`b32_sama_v2_verify.ts`](/GIT/tdd.md/blob/main/src/b32_sama_v2_verify.ts). | |
| 146 | 146 | The detector strips comments and quoted literals first, so those |
| 147 | 147 | four drop out. Seven real call sites remain: |
| 148 | 148 | |
content/blog/sama-v2-rust-project-ripgrep.md
+3
−3
| @@ -155,7 +155,7 @@ Derives from Law on the same edge set. | ||
| 155 | 155 | | **workingSetFit (50–500 LOC)** | **54.00% (measured, [ripgrep@4519153e](https://github.com/BurntSushi/ripgrep/commit/4519153e5e461527f4bca45b042fff45c4ec6fb9))** — originally estimated ~60% | **52.17% (measured, [dive@d6c69194](https://github.com/wagoodman/dive/commit/d6c691947f8fda635c952a17ee3b7555379d58f0))** — originally estimated ~80% | 80% | ~47% | |
| 156 | 156 | | violationCounts (sum) | ~50 estimated (Atomic + Modeled-tests under sibling-rule) | ~30 (estimated) | 0 | 17+ | |
| 157 | 157 | |
| 158 | -ripgrep's `workingSetFit` measures 54.00% (from the polyglot §5 emitter at [`scripts/measure-working-set.ts`](/GIT/syntaxai/tdd.md/blob/main/scripts/measure-working-set.ts), inclusive bounds [50, 500] LOC). The distribution: **100 .rs files** total, **16 under 50 LOC**, **54 in band**, **30 over 500 LOC** — appreciably more than the "19 big files" I eyeballed in the original audit. The over-cap list ranges from the textbook declarative-exempt catalog (`crates/core/flags/defs.rs` at 7,780 LOC) down to genuinely borderline files at 500–800 LOC like `crates/pcre2/src/matcher.rs` (506) and `crates/cli/src/decompress.rs` (533). | |
| 158 | +ripgrep's `workingSetFit` measures 54.00% (from the polyglot §5 emitter at [`scripts/measure-working-set.ts`](/GIT/tdd.md/blob/main/scripts/measure-working-set.ts), inclusive bounds [50, 500] LOC). The distribution: **100 .rs files** total, **16 under 50 LOC**, **54 in band**, **30 over 500 LOC** — appreciably more than the "19 big files" I eyeballed in the original audit. The over-cap list ranges from the textbook declarative-exempt catalog (`crates/core/flags/defs.rs` at 7,780 LOC) down to genuinely borderline files at 500–800 LOC like `crates/pcre2/src/matcher.rs` (506) and `crates/cli/src/decompress.rs` (533). | |
| 159 | 159 | |
| 160 | 160 | **And yet most of those files are appropriate to their content.** workingSetFit by itself doesn't say which side of the line each file falls on — that's what the [declarative-exemption dialect](/sama/v2#63-declarative-exemption-dialect) is for. The metric surfaces the property; the policy decides what to do with it. |
| 161 | 161 | |
| @@ -165,7 +165,7 @@ This is exactly the §5 intent. The metric surfaces a property; whether that pro | ||
| 165 | 165 | |
| 166 | 166 | ### graphDepth, measured: 5 (originally estimated ~5 — confirmed exactly) |
| 167 | 167 | |
| 168 | -The polyglot graphDepth emitter at [`scripts/measure-graph-depth.ts`](/GIT/syntaxai/tdd.md/blob/main/scripts/measure-graph-depth.ts) reads `ripgrep`'s root [`Cargo.toml`](https://github.com/BurntSushi/ripgrep/blob/4519153e5e461527f4bca45b042fff45c4ec6fb9/Cargo.toml), identifies workspace members + the root crate, parses each member's `[dependencies]` section (production deps only — `[dev-dependencies]` excluded from the runtime DAG), filters to workspace-internal deps (`path = "../foo"` or `workspace = true` cross-referenced against `[workspace.dependencies]`), and computes the longest crate-level chain. The result for `ripgrep@4519153e`: **10 workspace crates, 15 internal edges, longest dependency chain of depth 5**. | |
| 168 | +The polyglot graphDepth emitter at [`scripts/measure-graph-depth.ts`](/GIT/tdd.md/blob/main/scripts/measure-graph-depth.ts) reads `ripgrep`'s root [`Cargo.toml`](https://github.com/BurntSushi/ripgrep/blob/4519153e5e461527f4bca45b042fff45c4ec6fb9/Cargo.toml), identifies workspace members + the root crate, parses each member's `[dependencies]` section (production deps only — `[dev-dependencies]` excluded from the runtime DAG), filters to workspace-internal deps (`path = "../foo"` or `workspace = true` cross-referenced against `[workspace.dependencies]`), and computes the longest crate-level chain. The result for `ripgrep@4519153e`: **10 workspace crates, 15 internal edges, longest dependency chain of depth 5**. | |
| 169 | 169 | |
| 170 | 170 | **Hand-trace** (auditable per [/sama/v2 §0](/sama/v2)). The 10 workspace crates and their internal edges, extracted from `crates/*/Cargo.toml`: |
| 171 | 171 | |
| @@ -186,7 +186,7 @@ The polyglot graphDepth emitter at [`scripts/measure-graph-depth.ts`](/GIT/synta | ||
| 186 | 186 | |
| 187 | 187 | The longest path: **`ripgrep → grep → grep-printer → grep-searcher → grep-matcher`** — five crates, depth 5. Multiple paths reach depth 5 (e.g. `ripgrep → grep → grep-pcre2 → grep-matcher` is only depth 4; `ripgrep → grep → grep-searcher → grep-matcher` is depth 4; the printer-via-searcher chain is what wins). The audit's original estimate "(matcher → engine → searcher → printer → core)" turns out to describe the same chain reading bottom-up: `matcher ← searcher ← printer ← grep ← ripgrep`. Same five nodes, same depth, confirmed by measurement. |
| 188 | 188 | |
| 189 | -Module-granularity note: the polyglot graphDepth metric counts at the Rust crate level — each Cargo workspace member is one node. This is the natural Rust analog to the TS file-level metric (TS one module ≈ one file; Rust one module ≈ one crate). Semantic documented in [`src/b32_graph_depth_polyglot.ts`](/GIT/syntaxai/tdd.md/blob/main/src/b32_graph_depth_polyglot.ts). | |
| 189 | +Module-granularity note: the polyglot graphDepth metric counts at the Rust crate level — each Cargo workspace member is one node. This is the natural Rust analog to the TS file-level metric (TS one module ≈ one file; Rust one module ≈ one crate). Semantic documented in [`src/b32_graph_depth_polyglot.ts`](/GIT/tdd.md/blob/main/src/b32_graph_depth_polyglot.ts). | |
| 190 | 190 | |
| 191 | 191 | The contrast with `dive`'s measured depth 12 is itself interesting: ripgrep's crate-level graph is *flatter* than dive's package-directory graph, even though both are mature CLI codebases. Some of that is genuine — ripgrep's workspace is 10 crates organized as a clean DAG; dive's 27 package directories include many subdirectory hops that drive the chain longer. Some is granularity: a Rust crate often contains what a Go developer would split into multiple package directories. The two depths aren't directly comparable for "which codebase is deeper"; they ARE directly comparable as "graphDepth at each language's natural module unit," which is the spec's intent. |
| 192 | 192 | |
content/blog/sama-v2-sitemap-implementation-plan.md
+3
−3
| @@ -4,7 +4,7 @@ This site has 23 blog posts, 4 SAMA discipline pages, a `/sama/v2` spec, a verif | ||
| 4 | 4 | |
| 5 | 5 | This post is the **implementation plan** for `/sitemap.xml`, written *before* the code lands. The plan itself uses two artifacts worth pointing at: |
| 6 | 6 | |
| 7 | -1. **The `/goal` slash command in Claude Code.** The 38-line spec I'm working from is checked in at [`goal.md`](/GIT/syntaxai/tdd.md/blob/main/goal.md) — that's the exact `/goal` text fed to the agent. It declares *Done when*, *Constraints (anti-fudge)*, and *Load-bearing files to read FIRST*. The agent has to read those files before writing any code, and it has to satisfy every *Done when* clause before declaring done. | |
| 7 | +1. **The `/goal` slash command in Claude Code.** The 38-line spec I'm working from is checked in at [`goal.md`](/GIT/tdd.md/blob/main/goal.md) — that's the exact `/goal` text fed to the agent. It declares *Done when*, *Constraints (anti-fudge)*, and *Load-bearing files to read FIRST*. The agent has to read those files before writing any code, and it has to satisfy every *Done when* clause before declaring done. | |
| 8 | 8 | 2. **SAMA v2 itself.** The feature has to land conformant — `/sama/v2/verify` reports 7/7 ✓ on the live site, and breaking it on the way in is what the verifier catches. Every architectural decision below traces to a §4 check. |
| 9 | 9 | |
| 10 | 10 | The combination is what this post is really about. The `/goal` is the *what*; SAMA v2 is the *how*; the verifier is the *anti-fudge gate*. Each constrains the other in a way that turns "add a feature" into a mechanical exercise. |
| @@ -57,7 +57,7 @@ Load-bearing files to read FIRST: … | ||
| 57 | 57 | |
| 58 | 58 | The shape is what makes it useful. *Goal* is the human-readable intent. *Done when* is a checklist the agent will be evaluated against — and "all tests pass" is one bullet among many, not the only one. *Constraints* are the things the agent might be tempted to short-circuit (in this case: "URLs MUST come from existing registries — no second source of truth that can drift"). *Load-bearing files to read FIRST* prevents the agent from inventing structure that already exists — every existing helper, registry, and pattern is one Read tool call away, but only if the agent is told to look. |
| 59 | 59 | |
| 60 | -The full text of this feature's `/goal` is in [`goal.md`](/GIT/syntaxai/tdd.md/blob/main/goal.md). Cherry-picking the most load-bearing bullets: | |
| 60 | +The full text of this feature's `/goal` is in [`goal.md`](/GIT/tdd.md/blob/main/goal.md). Cherry-picking the most load-bearing bullets: | |
| 61 | 61 | |
| 62 | 62 | - *"URLs are derived from the registries (no hand-maintained slug list)"* — fixes the source-of-truth violation that would otherwise emerge over time. |
| 63 | 63 | - *"Sibling test covers: empty list → valid urlset with no `<url>` children; single URL with lastmod; single URL without lastmod; multiple URLs preserve order; XML-escape any `&` or `<` in URLs"* — fixes the Modeled-tests check (§4.3) before it gets a chance to fail. |
| @@ -80,7 +80,7 @@ Each of the seven [§4 conformance checks](/sama/v2#4-conformance) applies. The | ||
| 80 | 80 | | §4.6 The Law (§1.2) | d21 → b32 → a31. Strictly downward. No upward edge — the helper doesn't know what the handler does with its output. | |
| 81 | 81 | | §4.7 Consistency | The `b32_` prefix matches what the file actually contains: Layer 1 pure logic. No misnamed file, no logic at the wrong layer. | |
| 82 | 82 | |
| 83 | -That table isn't decorative. It's what the agent's `Done when` bullet *"`/sama/v2/verify` still reports 7/7 ✓"* expands into when run against the live site. Each row corresponds to one line in [`b32_sama_v2_verify.ts`](/GIT/syntaxai/tdd.md/blob/main/src/b32_sama_v2_verify.ts) that the agent doesn't get to touch. | |
| 83 | +That table isn't decorative. It's what the agent's `Done when` bullet *"`/sama/v2/verify` still reports 7/7 ✓"* expands into when run against the live site. Each row corresponds to one line in [`b32_sama_v2_verify.ts`](/GIT/tdd.md/blob/main/src/b32_sama_v2_verify.ts) that the agent doesn't get to touch. | |
| 84 | 84 | |
| 85 | 85 | ## Anti-fudge — the things this plan deliberately does NOT do |
| 86 | 86 | |
content/blog/sama-v2-workingset-cross-repo-baseline.md
+1
−1
| @@ -24,7 +24,7 @@ Corpus criteria: each project is a CLI tool, widely used (10k+ stars), mature (5 | ||
| 24 | 24 | |
| 25 | 25 | ## Methodology |
| 26 | 26 | |
| 27 | -The [polyglot §5 emitter](/GIT/syntaxai/tdd.md/blob/main/scripts/measure-working-set.ts) at `scripts/measure-working-set.ts` was used unchanged. The bounds **[50, 500] LOC inclusive** are imported from `WORKING_SET_MIN_LOC` and `WORKING_SET_MAX_LOC` in [`src/a31_sama_v2.ts`](/GIT/syntaxai/tdd.md/blob/main/src/a31_sama_v2.ts) — the same constants the `/sama/v2/verify` page uses against this site's own source. Single source of truth: the cross-repo numbers are computed against the *exact* band the spec defines. | |
| 27 | +The [polyglot §5 emitter](/GIT/tdd.md/blob/main/scripts/measure-working-set.ts) at `scripts/measure-working-set.ts` was used unchanged. The bounds **[50, 500] LOC inclusive** are imported from `WORKING_SET_MIN_LOC` and `WORKING_SET_MAX_LOC` in [`src/a31_sama_v2.ts`](/GIT/tdd.md/blob/main/src/a31_sama_v2.ts) — the same constants the `/sama/v2/verify` page uses against this site's own source. Single source of truth: the cross-repo numbers are computed against the *exact* band the spec defines. | |
| 28 | 28 | |
| 29 | 29 | LOC for each file = `content.split("\n").length`, matching the TS reference implementation byte-for-byte. Test-file exclusion rule: Go excludes `*_test.go` (mirroring TS's `*.test.ts` exclusion); Rust includes all `.rs` files because Rust's convention is inline `#[cfg(test)] mod tests` — formalised at [/sama/v2 §6.2 inline-tests dialect](/sama/v2#62-inline-tests-dialect). Skipped directories: `.git/`, `target/`, `vendor/`, `node_modules/`, all dotdirs. |
| 30 | 30 | |
content/home.md
+1
−1
| @@ -58,7 +58,7 @@ SAMA bundles those findings into four constraints a CI job can enforce. *Sorted* | ||
| 58 | 58 | |
| 59 | 59 | ## Datapoints on the same axes |
| 60 | 60 | |
| 61 | -Empirical baseline so far. The §4 score for this site is [computed live](/sama/v2/verify); the §4 scores for the other repos are hand-estimated. The **workingSetFit** column is now measured for the SAMA dogfood (this site) and seven non-SAMA mature compiled-language CLI tools by the polyglot §5 emitter at [`scripts/measure-working-set.ts`](/GIT/syntaxai/tdd.md/blob/main/scripts/measure-working-set.ts) — see the [seven-datapoint baseline post](/blog/sama-v2-workingset-cross-repo-baseline) for the full table, distribution, and hand-trace. | |
| 61 | +Empirical baseline so far. The §4 score for this site is [computed live](/sama/v2/verify); the §4 scores for the other repos are hand-estimated. The **workingSetFit** column is now measured for the SAMA dogfood (this site) and seven non-SAMA mature compiled-language CLI tools by the polyglot §5 emitter at [`scripts/measure-working-set.ts`](/GIT/tdd.md/blob/main/scripts/measure-working-set.ts) — see the [seven-datapoint baseline post](/blog/sama-v2-workingset-cross-repo-baseline) for the full table, distribution, and hand-trace. | |
| 62 | 62 | |
| 63 | 63 | | project | language | §4 score | workingSetFit | boundaryRatio | graphDepth | |
| 64 | 64 | |---|---|---|---|---|---| |
content/sama/v2.md
+2
−2
| @@ -157,7 +157,7 @@ This subsection pins how the §5 metrics are computed by the verifier at [/sama/ | ||
| 157 | 157 | - **Upper 500** — comfortably below the §4.5 Atomic 700-LOC cap, leaving headroom before a file approaches "split soon" territory. |
| 158 | 158 | - **Lower 50** — below this, a file is too small to be a substantive module; it is usually a type-only file, a stub, or a single helper that would read better inlined into a sibling. Type-only files (Layer 0 model shards) and minimal test fixtures fall here by design. They are acceptable but counted as "not in the working-set sweet spot" because they are not load-bearing modules. |
| 159 | 159 | |
| 160 | - Bounds are hard-coded constants `WORKING_SET_MIN_LOC = 50` and `WORKING_SET_MAX_LOC = 500` in [`src/a31_sama_v2.ts`](/GIT/syntaxai/tdd.md/blob/main/src/a31_sama_v2.ts) for v1 of the metrics emitter. Making them profile-configurable is a deliberate later step (requires extending the TOML subset parser to handle integer values). | |
| 160 | + Bounds are hard-coded constants `WORKING_SET_MIN_LOC = 50` and `WORKING_SET_MAX_LOC = 500` in [`src/a31_sama_v2.ts`](/GIT/tdd.md/blob/main/src/a31_sama_v2.ts) for v1 of the metrics emitter. Making them profile-configurable is a deliberate later step (requires extending the TOML subset parser to handle integer values). | |
| 161 | 161 | |
| 162 | 162 | - **violationCounts** = a record keyed by the seven §4 checks (`sorted`, `architecture`, `modeledTests`, `modeledBoundary`, `atomic`, `law`, `consistency`), each holding the integer count of violations that check produced on this run. Reported even when a check passes (value = 0) — this is §5's "trailing signal: which rules agents *almost* break." The verifier enumerates **all** violations per check (no short-circuit on first failure within a check), so the count is meaningful — not "1 if failed, 0 if passed". |
| 163 | 163 | |
| @@ -179,7 +179,7 @@ A raw grep across non-test `src/*.ts` finds seven hits matching `JSON.parse(` an | ||
| 179 | 179 | |
| 180 | 180 | Total: 7 parse-boundary call sites; all 7 fall under prefixes the profile maps to Layer 2. |
| 181 | 181 | |
| 182 | -`boundaryRatio = 7 / 7 = 1.0 = 100.0%` — which is exactly what [/sama/v2/verify](/sama/v2/verify) reports under §5 Core metrics. The hand count and the verifier's count match by construction: both consume `findParseBoundaryCallSites` in [`src/a31_sama_v2.ts`](/GIT/syntaxai/tdd.md/blob/main/src/a31_sama_v2.ts), and the Modeled-boundary check (#4) uses the same source of truth — so it cannot diverge. | |
| 182 | +`boundaryRatio = 7 / 7 = 1.0 = 100.0%` — which is exactly what [/sama/v2/verify](/sama/v2/verify) reports under §5 Core metrics. The hand count and the verifier's count match by construction: both consume `findParseBoundaryCallSites` in [`src/a31_sama_v2.ts`](/GIT/tdd.md/blob/main/src/a31_sama_v2.ts), and the Modeled-boundary check (#4) uses the same source of truth — so it cannot diverge. | |
| 183 | 183 | |
| 184 | 184 | --- |
| 185 | 185 | |
src/b32_git_url_redirect.test.ts
+54
−0
| @@ -0,0 +1,54 @@ | ||
| 1 | +import { describe, expect, test } from "bun:test"; | |
| 2 | +import { rewriteOldGitUrl } from "./b32_git_url_redirect.ts"; | |
| 3 | + | |
| 4 | +describe("rewriteOldGitUrl", () => { | |
| 5 | + test("rewrites old-form blob URL", () => { | |
| 6 | + expect( | |
| 7 | + rewriteOldGitUrl( | |
| 8 | + "/GIT/syntaxai/tdd.md/blob/main/src/b32_sama_v2_verify.ts", | |
| 9 | + ), | |
| 10 | + ).toBe("/GIT/tdd.md/blob/main/src/b32_sama_v2_verify.ts"); | |
| 11 | + }); | |
| 12 | + | |
| 13 | + test("rewrites old-form tree URL", () => { | |
| 14 | + expect(rewriteOldGitUrl("/GIT/syntaxai/tdd.md/tree/main")).toBe( | |
| 15 | + "/GIT/tdd.md/tree/main", | |
| 16 | + ); | |
| 17 | + }); | |
| 18 | + | |
| 19 | + test("rewrites old-form raw URL", () => { | |
| 20 | + expect( | |
| 21 | + rewriteOldGitUrl("/GIT/syntaxai/tdd.md/raw/main/sama.profile.toml"), | |
| 22 | + ).toBe("/GIT/tdd.md/raw/main/sama.profile.toml"); | |
| 23 | + }); | |
| 24 | + | |
| 25 | + test("rewrites old-form commit URL", () => { | |
| 26 | + expect( | |
| 27 | + rewriteOldGitUrl("/GIT/syntaxai/tdd.md/commit/abc1234"), | |
| 28 | + ).toBe("/GIT/tdd.md/commit/abc1234"); | |
| 29 | + }); | |
| 30 | + | |
| 31 | + test("rewrites old-form .diff URL (sha trailing .diff)", () => { | |
| 32 | + expect( | |
| 33 | + rewriteOldGitUrl("/GIT/syntaxai/tdd.md/commit/abc1234.diff"), | |
| 34 | + ).toBe("/GIT/tdd.md/commit/abc1234.diff"); | |
| 35 | + }); | |
| 36 | + | |
| 37 | + test("returns null for already-new URLs", () => { | |
| 38 | + expect(rewriteOldGitUrl("/GIT/tdd.md/blob/main/x.ts")).toBe(null); | |
| 39 | + }); | |
| 40 | + | |
| 41 | + test("returns null for other-org URLs", () => { | |
| 42 | + expect(rewriteOldGitUrl("/GIT/otherorg/repo/blob/main/x.ts")).toBe(null); | |
| 43 | + }); | |
| 44 | + | |
| 45 | + test("returns null for non-/GIT/ paths", () => { | |
| 46 | + expect(rewriteOldGitUrl("/blog/some-post")).toBe(null); | |
| 47 | + expect(rewriteOldGitUrl("/")).toBe(null); | |
| 48 | + }); | |
| 49 | + | |
| 50 | + test("returns null for incomplete old-form (just /GIT/syntaxai/tdd.md)", () => { | |
| 51 | + expect(rewriteOldGitUrl("/GIT/syntaxai/tdd.md")).toBe(null); | |
| 52 | + expect(rewriteOldGitUrl("/GIT/syntaxai/tdd.md/")).toBe(null); | |
| 53 | + }); | |
| 54 | +}); | |
src/b32_git_url_redirect.ts
+13
−0
| @@ -0,0 +1,13 @@ | ||
| 1 | +// b32 — Layer 1 pure helper: rewrite the legacy | |
| 2 | +// /GIT/syntaxai/tdd.md/<suffix> URL shape to /GIT/tdd.md/<suffix>. | |
| 3 | +// Pure: takes a pathname string, returns the new pathname or null | |
| 4 | +// when the input doesn't match the legacy shape. Called by the | |
| 5 | +// fallback handler to emit a 301 before the browse handler runs. | |
| 6 | + | |
| 7 | +const OLD_GIT_URL_PATTERN = /^\/GIT\/syntaxai\/tdd\.md\/(.+)$/; | |
| 8 | + | |
| 9 | +export const rewriteOldGitUrl = (pathname: string): string | null => { | |
| 10 | + const m = OLD_GIT_URL_PATTERN.exec(pathname); | |
| 11 | + if (m === null) return null; | |
| 12 | + return `/GIT/tdd.md/${m[1]}`; | |
| 13 | +}; | |
src/b51_render_commit.ts
+2
−2
| @@ -89,7 +89,7 @@ export const renderCommitView = async (params: { | ||
| 89 | 89 | const parentLinks = detail.parents.length === 0 |
| 90 | 90 | ? `<span class="commit-meta-empty">no parent (root commit)</span>` |
| 91 | 91 | : detail.parents.map((p) => |
| 92 | - `<a class="commit-parent" href="/GIT/${escape(owner)}/${escape(repo)}/commit/${escape(p)}"><code>${escape(shortSha(p))}</code></a>`, | |
| 92 | + `<a class="commit-parent" href="/GIT/${escape(repo)}/commit/${escape(p)}"><code>${escape(shortSha(p))}</code></a>`, | |
| 93 | 93 | ).join(" · "); |
| 94 | 94 | |
| 95 | 95 | const totalAdded = diff.files.reduce((s, f) => s + f.added, 0); |
| @@ -113,7 +113,7 @@ export const renderCommitView = async (params: { | ||
| 113 | 113 | ${filesSummary} |
| 114 | 114 | ${diff.files.map(renderFile).join("")} |
| 115 | 115 | <p class="commit-footer"> |
| 116 | - <a href="/GIT/${escape(owner)}/${escape(repo)}/commit/${escape(detail.sha)}.diff">raw .diff</a> | |
| 116 | + <a href="/GIT/${escape(repo)}/commit/${escape(detail.sha)}.diff">raw .diff</a> | |
| 117 | 117 | </p> |
| 118 | 118 | </main>`; |
| 119 | 119 | |
src/b51_render_edit.ts
+1
−1
| @@ -24,7 +24,7 @@ const shortSha = (sha: string): string => sha.slice(0, 7); | ||
| 24 | 24 | // renders it through tdd.md's chrome — visitor never leaves the main |
| 25 | 25 | // domain. |
| 26 | 26 | const tddCommitUrl = (sha: string): string => |
| 27 | - `/GIT/syntaxai/tdd.md/commit/${sha}`; | |
| 27 | + `/GIT/tdd.md/commit/${sha}`; | |
| 28 | 28 | |
| 29 | 29 | // -------- /edit/:section/:slug — form for the admin -------- |
| 30 | 30 | |
src/b51_render_repo.test.ts
+1
−1
| @@ -1,5 +1,5 @@ | ||
| 1 | 1 | // Sibling test for c51_render_repo.ts (Layer 1, render). End-to-end |
| 2 | -// shape covered by /GIT/syntaxai/tdd.md/tree|blob/main e2e specs. | |
| 2 | +// shape covered by /GIT/tdd.md/tree|blob/main e2e specs. | |
| 3 | 3 | // This pins the export surface. |
| 4 | 4 | |
| 5 | 5 | import { describe, test, expect } from "bun:test"; |
src/b51_render_repo.ts
+9
−9
| @@ -1,5 +1,5 @@ | ||
| 1 | 1 | // c51 — UI: tree listing + blob viewer for the local bare repo. |
| 2 | -// Visited at /GIT/:owner/:repo/tree/:ref/<path> and /blob/:ref/<path>. | |
| 2 | +// Visited at /GIT/:repo/tree/:ref/<path> and /blob/:ref/<path>. | |
| 3 | 3 | // Renders through tdd.md's chrome (renderPage with bodyHtml). Markdown |
| 4 | 4 | // blobs get parsed via marked; everything else is rendered as |
| 5 | 5 | // preformatted source. |
| @@ -20,8 +20,8 @@ const renderBreadcrumb = (params: { | ||
| 20 | 20 | asBlob?: boolean; |
| 21 | 21 | }): string => { |
| 22 | 22 | const { owner, repo, ref, path, asBlob } = params; |
| 23 | - const repoLink = `<a href="/GIT/${escape(owner)}/${escape(repo)}/tree/${escape(ref)}"><strong>${escape(owner)}/${escape(repo)}</strong></a>`; | |
| 24 | - const refLink = `<a class="commit-meta-pill" href="/GIT/${escape(owner)}/${escape(repo)}/tree/${escape(ref)}"><code>${escape(ref)}</code></a>`; | |
| 23 | + const repoLink = `<a href="/GIT/${escape(repo)}/tree/${escape(ref)}"><strong>${escape(owner)}/${escape(repo)}</strong></a>`; | |
| 24 | + const refLink = `<a class="commit-meta-pill" href="/GIT/${escape(repo)}/tree/${escape(ref)}"><code>${escape(ref)}</code></a>`; | |
| 25 | 25 | if (path === "") return `<p class="commit-breadcrumb">${repoLink} · ${refLink}</p>`; |
| 26 | 26 | |
| 27 | 27 | const segments = path.split("/"); |
| @@ -33,7 +33,7 @@ const renderBreadcrumb = (params: { | ||
| 33 | 33 | // For tree view, every segment links to the tree at that depth. |
| 34 | 34 | const isLastFile = asBlob && i === lastIdx; |
| 35 | 35 | if (isLastFile) return `<code>${escape(seg)}</code>`; |
| 36 | - return `<a href="/GIT/${escape(owner)}/${escape(repo)}/tree/${escape(ref)}/${escape(so_far)}"><code>${escape(seg)}</code></a>`; | |
| 36 | + return `<a href="/GIT/${escape(repo)}/tree/${escape(ref)}/${escape(so_far)}"><code>${escape(seg)}</code></a>`; | |
| 37 | 37 | }) |
| 38 | 38 | .join(" / "); |
| 39 | 39 | return `<p class="commit-breadcrumb">${repoLink} · ${refLink} · ${links}</p>`; |
| @@ -62,7 +62,7 @@ const renderTreeRow = (params: { | ||
| 62 | 62 | entry.type === "commit" ? "🔗" : // submodule |
| 63 | 63 | "📄"; |
| 64 | 64 | const kind = entry.type === "tree" ? "tree" : "blob"; |
| 65 | - const href = `/GIT/${escape(owner)}/${escape(repo)}/${kind}/${escape(ref)}/${escape(childPath)}`; | |
| 65 | + const href = `/GIT/${escape(repo)}/${kind}/${escape(ref)}/${escape(childPath)}`; | |
| 66 | 66 | return `<tr class="repo-tree-row repo-tree-row-${entry.type}"> |
| 67 | 67 | <td class="repo-tree-icon">${icon}</td> |
| 68 | 68 | <td class="repo-tree-name"><a href="${href}">${escape(entry.name)}</a></td> |
| @@ -84,8 +84,8 @@ export const renderRepoTree = async (params: { | ||
| 84 | 84 | : (() => { |
| 85 | 85 | const parentPath = path.includes("/") ? path.slice(0, path.lastIndexOf("/")) : ""; |
| 86 | 86 | const upHref = parentPath === "" |
| 87 | - ? `/GIT/${escape(owner)}/${escape(repo)}/tree/${escape(ref)}` | |
| 88 | - : `/GIT/${escape(owner)}/${escape(repo)}/tree/${escape(ref)}/${escape(parentPath)}`; | |
| 87 | + ? `/GIT/${escape(repo)}/tree/${escape(ref)}` | |
| 88 | + : `/GIT/${escape(repo)}/tree/${escape(ref)}/${escape(parentPath)}`; | |
| 89 | 89 | return `<tr class="repo-tree-row repo-tree-row-up"><td class="repo-tree-icon">⬆</td><td class="repo-tree-name"><a href="${upHref}">..</a></td><td></td></tr>`; |
| 90 | 90 | })(); |
| 91 | 91 | const rows = entries.length === 0 |
| @@ -135,8 +135,8 @@ export const renderRepoBlob = async (params: { | ||
| 135 | 135 | <code class="repo-blob-path">${escape(filename)}</code> |
| 136 | 136 | <span class="repo-blob-meta">${content.split("\n").length} lines · ${content.length} bytes</span> |
| 137 | 137 | <span class="repo-blob-actions"> |
| 138 | - <a href="/GIT/${escape(owner)}/${escape(repo)}/raw/${escape(ref)}/${escape(path)}">raw</a> | |
| 139 | - ${isMarkdown(path) ? `· <a href="/GIT/${escape(owner)}/${escape(repo)}/blob/${escape(ref)}/${escape(path)}?source=1">source</a>` : ""} | |
| 138 | + <a href="/GIT/${escape(repo)}/raw/${escape(ref)}/${escape(path)}">raw</a> | |
| 139 | + ${isMarkdown(path) ? `· <a href="/GIT/${escape(repo)}/blob/${escape(ref)}/${escape(path)}?source=1">source</a>` : ""} | |
| 140 | 140 | </span> |
| 141 | 141 | </header> |
| 142 | 142 | ${bodyHtml} |
src/d21_app.ts
+1
−1
| @@ -483,7 +483,7 @@ ${rows} | ||
| 483 | 483 | // SAMA-native commit view — Bun-rendered alternative to Forgejo's |
| 484 | 484 | // /<owner>/<repo>/commit/<sha> page. The :sha param may carry a |
| 485 | 485 | // trailing ".diff" which the handler handles inline. |
| 486 | - "/GIT/:owner/:repo/commit/:sha": commitViewHandler, | |
| 486 | + "/GIT/:repo/commit/:sha": commitViewHandler, | |
| 487 | 487 | |
| 488 | 488 | "/auth/github/start": (req) => startGithubOauth(req), |
| 489 | 489 | |
src/d21_handlers_commit_view.ts
+24
−20
| @@ -1,13 +1,13 @@ | ||
| 1 | 1 | // c21 — handler: SAMA-native commit view at |
| 2 | -// GET /GIT/:owner/:repo/commit/:sha | |
| 2 | +// GET /GIT/:repo/commit/:sha | |
| 3 | 3 | // and a raw-diff sibling at |
| 4 | -// GET /GIT/:owner/:repo/commit/:sha.diff | |
| 4 | +// GET /GIT/:repo/commit/:sha.diff | |
| 5 | 5 | // |
| 6 | 6 | // Composes c14 (Forgejo HTTP), c31 (diff parser), c51 (render). The |
| 7 | 7 | // route prefix is uppercase /GIT/ to make it visually distinct from |
| 8 | -// the markdown content sections (/sama, /blog, /guides). Visitors who | |
| 9 | -// land on git.tdd.md are bounced here by the deploy-time tunnel rule | |
| 10 | -// (out of scope for this handler — handler just owns the rendering). | |
| 8 | +// the markdown content sections (/sama, /blog, /guides). Owner is | |
| 9 | +// implicit (single-tenant — LIVE_REPO_OWNER) and never appears in | |
| 10 | +// the URL surface. | |
| 11 | 11 | |
| 12 | 12 | import { renderNotFound, htmlResponse } from "./b51_render_layout.ts"; |
| 13 | 13 | import { getCommit, getCommitDiff } from "./c14_git.ts"; |
| @@ -15,38 +15,37 @@ import { LIVE_REPO_OWNER, LIVE_REPO_NAME } from "./a31_site_config.ts"; | ||
| 15 | 15 | import { parseUnifiedDiff } from "./a31_diff_parse.ts"; |
| 16 | 16 | import { renderCommitView } from "./b51_render_commit.ts"; |
| 17 | 17 | |
| 18 | -// Owner/repo + sha shape — paranoid because these go straight into a | |
| 19 | -// Forgejo URL. Owner/repo allow letters/digits/hyphens/underscores/dots; | |
| 18 | +// Repo + sha shape — paranoid because these go straight into a | |
| 19 | +// Forgejo URL. Repo allows letters/digits/hyphens/underscores/dots; | |
| 20 | 20 | // sha is hex 7-64 (Forgejo accepts shortened SHAs but our render assumes |
| 21 | 21 | // full ones because we use them in URLs). |
| 22 | 22 | const SAFE_OWNER_REPO = /^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/; |
| 23 | 23 | const SAFE_SHA = /^[a-f0-9]{7,64}$/; |
| 24 | 24 | |
| 25 | -const isValid = (owner: string, repo: string, sha: string): boolean => | |
| 26 | - SAFE_OWNER_REPO.test(owner) && SAFE_OWNER_REPO.test(repo) && SAFE_SHA.test(sha); | |
| 25 | +const isValid = (repo: string, sha: string): boolean => | |
| 26 | + SAFE_OWNER_REPO.test(repo) && SAFE_SHA.test(sha); | |
| 27 | 27 | |
| 28 | 28 | export const commitViewHandler = async ( |
| 29 | - req: Request & { params: { owner: string; repo: string; sha: string } }, | |
| 29 | + req: Request & { params: { repo: string; sha: string } }, | |
| 30 | 30 | ): Promise<Response> => { |
| 31 | - const { owner, repo } = req.params; | |
| 31 | + const { repo } = req.params; | |
| 32 | 32 | // The :sha param may carry a trailing ".diff" because the route |
| 33 | 33 | // pattern doesn't have a separate one. Normalise + branch. |
| 34 | 34 | const rawSha = req.params.sha; |
| 35 | 35 | const wantsDiff = rawSha.endsWith(".diff"); |
| 36 | 36 | const sha = wantsDiff ? rawSha.slice(0, -5) : rawSha; |
| 37 | - const fullPath = `/GIT/${owner}/${repo}/commit/${rawSha}`; | |
| 37 | + const fullPath = `/GIT/${repo}/commit/${rawSha}`; | |
| 38 | 38 | |
| 39 | - if (!isValid(owner, repo, sha)) { | |
| 39 | + if (!isValid(repo, sha)) { | |
| 40 | 40 | const html = await renderNotFound(fullPath); |
| 41 | 41 | return htmlResponse(html, 404); |
| 42 | 42 | } |
| 43 | 43 | |
| 44 | - // /GIT/ now serves only syntaxai/tdd.md (our local bare repo via | |
| 45 | - // c14_git). Other (owner, repo) pairs would historically have been | |
| 46 | - // proxied to Forgejo for agent katas — that's a separate concern | |
| 47 | - // and currently 404s. If we want it back, add a Forgejo fallback | |
| 48 | - // branch here keyed on the owner/repo pair. | |
| 49 | - if (owner !== LIVE_REPO_OWNER || repo !== LIVE_REPO_NAME) { | |
| 44 | + // /GIT/ now serves only the local bare repo (LIVE_REPO_NAME via | |
| 45 | + // c14_git). Other repos would historically have been proxied to | |
| 46 | + // Forgejo for agent katas — that's a separate concern and | |
| 47 | + // currently 404s. | |
| 48 | + if (repo !== LIVE_REPO_NAME) { | |
| 50 | 49 | const html = await renderNotFound(fullPath); |
| 51 | 50 | return htmlResponse(html, 404); |
| 52 | 51 | } |
| @@ -85,6 +84,11 @@ export const commitViewHandler = async ( | ||
| 85 | 84 | committerDate: commit.committerDate, |
| 86 | 85 | message: commit.message, |
| 87 | 86 | }; |
| 88 | - const html = await renderCommitView({ owner, repo, detail, diff }); | |
| 87 | + const html = await renderCommitView({ | |
| 88 | + owner: LIVE_REPO_OWNER, | |
| 89 | + repo, | |
| 90 | + detail, | |
| 91 | + diff, | |
| 92 | + }); | |
| 89 | 93 | return htmlResponse(html); |
| 90 | 94 | }; |
src/d21_handlers_fallback.ts
+21
−6
| @@ -22,6 +22,7 @@ import { | ||
| 22 | 22 | parseRepoBrowsePath, |
| 23 | 23 | repoBrowseHandler, |
| 24 | 24 | } from "./d21_handlers_repo_browse.ts"; |
| 25 | +import { rewriteOldGitUrl } from "./b32_git_url_redirect.ts"; | |
| 25 | 26 | |
| 26 | 27 | const isGitProtocol = (pathname: string, search: URLSearchParams): boolean => { |
| 27 | 28 | if (pathname.includes(".git/") || pathname.endsWith(".git")) return true; |
| @@ -122,22 +123,36 @@ export const appFetch = async (req: Request): Promise<Response> => { | ||
| 122 | 123 | }); |
| 123 | 124 | } |
| 124 | 125 | |
| 125 | - // SAMA-native repo browse at /GIT/:owner/:repo/{tree,blob,raw}/:ref/<path>. | |
| 126 | + // Legacy /GIT/syntaxai/tdd.md/<suffix> URLs permanent-redirect to | |
| 127 | + // the new owner-less shape. MUST sit before the browse-match below | |
| 128 | + // so the legacy URL never reaches the browse handler. One regex | |
| 129 | + // covers every kind (tree/blob/raw/commit) + every future path. | |
| 130 | + const newGitPath = rewriteOldGitUrl(url.pathname); | |
| 131 | + if (newGitPath !== null) { | |
| 132 | + return new Response(null, { | |
| 133 | + status: 301, | |
| 134 | + headers: { | |
| 135 | + Location: newGitPath, | |
| 136 | + "Cache-Control": "public, max-age=86400", | |
| 137 | + }, | |
| 138 | + }); | |
| 139 | + } | |
| 140 | + | |
| 141 | + // SAMA-native repo browse at /GIT/:repo/{tree,blob,raw}/:ref/<path>. | |
| 126 | 142 | // The wildcard path needs more flexibility than Bun's :param routes |
| 127 | 143 | // give us (no slashes), so we match in the fallback fetch instead. |
| 128 | 144 | const gitBrowseMatch = url.pathname.match( |
| 129 | - /^\/GIT\/([A-Za-z0-9][A-Za-z0-9._-]+)\/([A-Za-z0-9][A-Za-z0-9._-]+)\/(.+)$/, | |
| 145 | + /^\/GIT\/([A-Za-z0-9][A-Za-z0-9._-]+)\/(.+)$/, | |
| 130 | 146 | ); |
| 131 | 147 | if (gitBrowseMatch) { |
| 132 | - const owner = gitBrowseMatch[1]!; | |
| 133 | - const repo = gitBrowseMatch[2]!; | |
| 134 | - const suffix = gitBrowseMatch[3]!; | |
| 148 | + const repo = gitBrowseMatch[1]!; | |
| 149 | + const suffix = gitBrowseMatch[2]!; | |
| 135 | 150 | // Skip the commit/<sha> shape — that's c21_handlers_commit_view's |
| 136 | 151 | // turf and lives as an explicit Bun.serve route in c21_app. |
| 137 | 152 | if (!suffix.startsWith("commit/")) { |
| 138 | 153 | const target = parseRepoBrowsePath(suffix); |
| 139 | 154 | if (target !== null) { |
| 140 | - return repoBrowseHandler(req, owner, repo, target); | |
| 155 | + return repoBrowseHandler(req, repo, target); | |
| 141 | 156 | } |
| 142 | 157 | } |
| 143 | 158 | } |
src/d21_handlers_repo_browse.ts
+8
−10
| @@ -23,11 +23,10 @@ const SAFE_OWNER_REPO = /^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/; | ||
| 23 | 23 | // would clash with the wildcard path matching). |
| 24 | 24 | const SAFE_REF = /^[A-Za-z0-9][A-Za-z0-9._-]{0,49}$/; |
| 25 | 25 | |
| 26 | -const isAllowedRepo = (owner: string, repo: string): boolean => | |
| 27 | - owner === LIVE_REPO_OWNER && | |
| 28 | - repo === LIVE_REPO_NAME && | |
| 29 | - SAFE_OWNER_REPO.test(owner) && | |
| 30 | - SAFE_OWNER_REPO.test(repo); | |
| 26 | +// Single-tenant: the only allowed repo is LIVE_REPO_NAME. Owner is | |
| 27 | +// implicit (LIVE_REPO_OWNER) and no longer carried in the URL. | |
| 28 | +const isAllowedRepo = (repo: string): boolean => | |
| 29 | + repo === LIVE_REPO_NAME && SAFE_OWNER_REPO.test(repo); | |
| 31 | 30 | |
| 32 | 31 | // Only allow paths that look like ordinary repo entries — letters, |
| 33 | 32 | // digits, hyphens, underscores, dots, slashes. Reject anything with |
| @@ -68,13 +67,12 @@ export const parseRepoBrowsePath = (suffix: string): RepoBrowseTarget | null => | ||
| 68 | 67 | |
| 69 | 68 | export const repoBrowseHandler = async ( |
| 70 | 69 | req: Request, |
| 71 | - owner: string, | |
| 72 | 70 | repo: string, |
| 73 | 71 | target: RepoBrowseTarget, |
| 74 | 72 | ): Promise<Response> => { |
| 75 | - const fullPath = `/GIT/${owner}/${repo}/${target.kind}/${target.ref}${target.path ? "/" + target.path : ""}`; | |
| 73 | + const fullPath = `/GIT/${repo}/${target.kind}/${target.ref}${target.path ? "/" + target.path : ""}`; | |
| 76 | 74 | |
| 77 | - if (!isAllowedRepo(owner, repo)) { | |
| 75 | + if (!isAllowedRepo(repo)) { | |
| 78 | 76 | const html = await renderNotFound(fullPath); |
| 79 | 77 | return htmlResponse(html, 404); |
| 80 | 78 | } |
| @@ -86,7 +84,7 @@ export const repoBrowseHandler = async ( | ||
| 86 | 84 | return htmlResponse(html, 404); |
| 87 | 85 | } |
| 88 | 86 | const html = await renderRepoTree({ |
| 89 | - owner, | |
| 87 | + owner: LIVE_REPO_OWNER, | |
| 90 | 88 | repo, |
| 91 | 89 | ref: target.ref, |
| 92 | 90 | path: target.path, |
| @@ -102,7 +100,7 @@ export const repoBrowseHandler = async ( | ||
| 102 | 100 | return htmlResponse(html, 404); |
| 103 | 101 | } |
| 104 | 102 | const html = await renderRepoBlob({ |
| 105 | - owner, | |
| 103 | + owner: LIVE_REPO_OWNER, | |
| 106 | 104 | repo, |
| 107 | 105 | ref: target.ref, |
| 108 | 106 | path: target.path, |
src/d21_handlers_sama.ts
+1
−1
| @@ -134,7 +134,7 @@ const renderV2Report = (report: SamaV2Report, metrics: SamaV2Metrics): string => | ||
| 134 | 134 | |
| 135 | 135 | > ${summary} |
| 136 | 136 | |
| 137 | -The verifier in [\`src/b32_sama_v2_verify.ts\`](/GIT/syntaxai/tdd.md/blob/main/src/b32_sama_v2_verify.ts) ingests [\`sama.profile.toml\`](/GIT/syntaxai/tdd.md/blob/main/sama.profile.toml) and runs the seven §4 conformance checks against the current source tree on this server. No clone, no token; the server reads its own \`src/\` and the committed profile, runs the same logic the sibling unit tests cover, and renders the verdict below. The §5 core metrics emitter ([\`src/b32_sama_v2_metrics.ts\`](/GIT/syntaxai/tdd.md/blob/main/src/b32_sama_v2_metrics.ts)) runs on the same input and shares the parse-boundary detector with the Modeled-boundary check. | |
| 137 | +The verifier in [\`src/b32_sama_v2_verify.ts\`](/GIT/tdd.md/blob/main/src/b32_sama_v2_verify.ts) ingests [\`sama.profile.toml\`](/GIT/tdd.md/blob/main/sama.profile.toml) and runs the seven §4 conformance checks against the current source tree on this server. No clone, no token; the server reads its own \`src/\` and the committed profile, runs the same logic the sibling unit tests cover, and renders the verdict below. The §5 core metrics emitter ([\`src/b32_sama_v2_metrics.ts\`](/GIT/tdd.md/blob/main/src/b32_sama_v2_metrics.ts)) runs on the same input and shares the parse-boundary detector with the Modeled-boundary check. | |
| 138 | 138 | |
| 139 | 139 | | check | verdict | examined | |
| 140 | 140 | |---|---|---| |