df9d41adb35da5aed775a8b857592a958cc0bf47 diff --git a/content/blog/sama-v2-git-url-refactor-plan.md b/content/blog/sama-v2-git-url-refactor-plan.md index 31cd11031eb8ceef8a9615139fed950dfc6e122d..fa06aa51a8d362f04779d69c0599b7a0cab92a2b 100644 --- a/content/blog/sama-v2-git-url-refactor-plan.md +++ b/content/blog/sama-v2-git-url-refactor-plan.md @@ -15,7 +15,7 @@ Nine characters shorter. The change is small but the workflow it sits inside is ## Why dropping the owner is safe -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): +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): ```ts 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 - **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. - **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. -- **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. +- **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. - **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. - **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. diff --git a/content/blog/sama-v2-go-project-dive.md b/content/blog/sama-v2-go-project-dive.md index c1606c03b106b652e7cd533fa8a0bee99b4a522c..6b0c8d989a53f4902e726e2280df97fbf06f3d48 100644 --- a/content/blog/sama-v2-go-project-dive.md +++ b/content/blog/sama-v2-go-project-dive.md @@ -153,7 +153,7 @@ Derives from Law. No file's declared layer is contradicted by what it imports. **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. -**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. +**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. 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. @@ -161,13 +161,13 @@ The original ~80% estimate was wrong, and wrong in a direction casual eyeballing ### graphDepth, measured: 12 (originally estimated ~5) -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**. +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**. 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. 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. -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). +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). ## What `dive` would look like at 7/7 — the last 30% diff --git a/content/blog/sama-v2-metrics-emitter.md b/content/blog/sama-v2-metrics-emitter.md index ad38fa25276eb9b2b6af2288048f3bbb5f578939..f7322b4c2a920c0c7b1e65737311682f725200ae 100644 --- a/content/blog/sama-v2-metrics-emitter.md +++ b/content/blog/sama-v2-metrics-emitter.md @@ -116,7 +116,7 @@ verifier can say "0 violations" while the metric says "30% of boundaries are in Layer 1" — contradictory pictures of the same code. So the detector now lives in one place — `findParseBoundaryCallSites` -in [`src/a31_sama_v2.ts`](/GIT/syntaxai/tdd.md/blob/main/src/a31_sama_v2.ts) +in [`src/a31_sama_v2.ts`](/GIT/tdd.md/blob/main/src/a31_sama_v2.ts) (Layer 0, pure). The verifier consumes it. The metric consumes it. They share the regex, the comment/string-literal stripping, the file 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 `JSON.parse(` and `new URL(` returns eleven hits. Four of them are inside comments or string literals — *"`JSON.parse()` constructors must not appear in..."* in the docstring of -[`c14_request_parse.ts`](/GIT/syntaxai/tdd.md/blob/main/src/c14_request_parse.ts), +[`c14_request_parse.ts`](/GIT/tdd.md/blob/main/src/c14_request_parse.ts), *"parsing as `JSON.parse(` of arbitrary input..."* in the comment header of -[`b32_sama_v2_verify.ts`](/GIT/syntaxai/tdd.md/blob/main/src/b32_sama_v2_verify.ts). +[`b32_sama_v2_verify.ts`](/GIT/tdd.md/blob/main/src/b32_sama_v2_verify.ts). The detector strips comments and quoted literals first, so those four drop out. Seven real call sites remain: diff --git a/content/blog/sama-v2-rust-project-ripgrep.md b/content/blog/sama-v2-rust-project-ripgrep.md index 189a58165c47a0afc6a8dd9b12834ebf8b352792..32560090df242af585456b7ac3af749ba8343011 100644 --- a/content/blog/sama-v2-rust-project-ripgrep.md +++ b/content/blog/sama-v2-rust-project-ripgrep.md @@ -155,7 +155,7 @@ Derives from Law on the same edge set. | **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% | | violationCounts (sum) | ~50 estimated (Atomic + Modeled-tests under sibling-rule) | ~30 (estimated) | 0 | 17+ | -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). +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). **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. @@ -165,7 +165,7 @@ This is exactly the §5 intent. The metric surfaces a property; whether that pro ### graphDepth, measured: 5 (originally estimated ~5 — confirmed exactly) -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**. +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**. **Hand-trace** (auditable per [/sama/v2 §0](/sama/v2)). The 10 workspace crates and their internal edges, extracted from `crates/*/Cargo.toml`: @@ -186,7 +186,7 @@ The polyglot graphDepth emitter at [`scripts/measure-graph-depth.ts`](/GIT/synta 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. -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). +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). 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. diff --git a/content/blog/sama-v2-sitemap-implementation-plan.md b/content/blog/sama-v2-sitemap-implementation-plan.md index 0ff340a8d263b79c5df02c616220430958b0d75b..afc8a91b3cfb71c123ec18393f60f532e865441e 100644 --- a/content/blog/sama-v2-sitemap-implementation-plan.md +++ b/content/blog/sama-v2-sitemap-implementation-plan.md @@ -4,7 +4,7 @@ This site has 23 blog posts, 4 SAMA discipline pages, a `/sama/v2` spec, a verif This post is the **implementation plan** for `/sitemap.xml`, written *before* the code lands. The plan itself uses two artifacts worth pointing at: -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. +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. 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. 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: … 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. -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: +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: - *"URLs are derived from the registries (no hand-maintained slug list)"* — fixes the source-of-truth violation that would otherwise emerge over time. - *"Sibling test covers: empty list → valid urlset with no `` 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 | §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. | | §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. | -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. +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. ## Anti-fudge — the things this plan deliberately does NOT do diff --git a/content/blog/sama-v2-workingset-cross-repo-baseline.md b/content/blog/sama-v2-workingset-cross-repo-baseline.md index 65dcbe5faef0b87d73652bcc6b62a75b54691b41..8792be88146997e1b05b8fbd54bd8b896ceb34dd 100644 --- a/content/blog/sama-v2-workingset-cross-repo-baseline.md +++ b/content/blog/sama-v2-workingset-cross-repo-baseline.md @@ -24,7 +24,7 @@ Corpus criteria: each project is a CLI tool, widely used (10k+ stars), mature (5 ## Methodology -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. +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. 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. diff --git a/content/home.md b/content/home.md index f4692e2c1ee001a1d57431b387359d9396d5eef6..6a7cfcccab056d1a0477349fac7ef78af50e6149 100644 --- a/content/home.md +++ b/content/home.md @@ -58,7 +58,7 @@ SAMA bundles those findings into four constraints a CI job can enforce. *Sorted* ## Datapoints on the same axes -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. +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. | project | language | §4 score | workingSetFit | boundaryRatio | graphDepth | |---|---|---|---|---|---| diff --git a/content/sama/v2.md b/content/sama/v2.md index cdfe8c9978ef20de98c5e01ed85a2ae21f18e529..8be5db22aabe702a090b7e3f0fee158af5d18a6a 100644 --- a/content/sama/v2.md +++ b/content/sama/v2.md @@ -157,7 +157,7 @@ This subsection pins how the §5 metrics are computed by the verifier at [/sama/ - **Upper 500** — comfortably below the §4.5 Atomic 700-LOC cap, leaving headroom before a file approaches "split soon" territory. - **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. - 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). + 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). - **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". @@ -179,7 +179,7 @@ A raw grep across non-test `src/*.ts` finds seven hits matching `JSON.parse(` an Total: 7 parse-boundary call sites; all 7 fall under prefixes the profile maps to Layer 2. -`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. +`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. --- diff --git a/src/b32_git_url_redirect.test.ts b/src/b32_git_url_redirect.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..29d9c9c890e2a17f46a0f9f92b221a324b46e0f5 --- /dev/null +++ b/src/b32_git_url_redirect.test.ts @@ -0,0 +1,54 @@ +import { describe, expect, test } from "bun:test"; +import { rewriteOldGitUrl } from "./b32_git_url_redirect.ts"; + +describe("rewriteOldGitUrl", () => { + test("rewrites old-form blob URL", () => { + expect( + rewriteOldGitUrl( + "/GIT/syntaxai/tdd.md/blob/main/src/b32_sama_v2_verify.ts", + ), + ).toBe("/GIT/tdd.md/blob/main/src/b32_sama_v2_verify.ts"); + }); + + test("rewrites old-form tree URL", () => { + expect(rewriteOldGitUrl("/GIT/syntaxai/tdd.md/tree/main")).toBe( + "/GIT/tdd.md/tree/main", + ); + }); + + test("rewrites old-form raw URL", () => { + expect( + rewriteOldGitUrl("/GIT/syntaxai/tdd.md/raw/main/sama.profile.toml"), + ).toBe("/GIT/tdd.md/raw/main/sama.profile.toml"); + }); + + test("rewrites old-form commit URL", () => { + expect( + rewriteOldGitUrl("/GIT/syntaxai/tdd.md/commit/abc1234"), + ).toBe("/GIT/tdd.md/commit/abc1234"); + }); + + test("rewrites old-form .diff URL (sha trailing .diff)", () => { + expect( + rewriteOldGitUrl("/GIT/syntaxai/tdd.md/commit/abc1234.diff"), + ).toBe("/GIT/tdd.md/commit/abc1234.diff"); + }); + + test("returns null for already-new URLs", () => { + expect(rewriteOldGitUrl("/GIT/tdd.md/blob/main/x.ts")).toBe(null); + }); + + test("returns null for other-org URLs", () => { + expect(rewriteOldGitUrl("/GIT/otherorg/repo/blob/main/x.ts")).toBe(null); + }); + + test("returns null for non-/GIT/ paths", () => { + expect(rewriteOldGitUrl("/blog/some-post")).toBe(null); + expect(rewriteOldGitUrl("/")).toBe(null); + }); + + test("returns null for incomplete old-form (just /GIT/syntaxai/tdd.md)", () => { + expect(rewriteOldGitUrl("/GIT/syntaxai/tdd.md")).toBe(null); + expect(rewriteOldGitUrl("/GIT/syntaxai/tdd.md/")).toBe(null); + }); +}); diff --git a/src/b32_git_url_redirect.ts b/src/b32_git_url_redirect.ts new file mode 100644 index 0000000000000000000000000000000000000000..33dbcf0cc7e99862eff6cd4044b5d23f98d4b13e --- /dev/null +++ b/src/b32_git_url_redirect.ts @@ -0,0 +1,13 @@ +// b32 — Layer 1 pure helper: rewrite the legacy +// /GIT/syntaxai/tdd.md/ URL shape to /GIT/tdd.md/. +// Pure: takes a pathname string, returns the new pathname or null +// when the input doesn't match the legacy shape. Called by the +// fallback handler to emit a 301 before the browse handler runs. + +const OLD_GIT_URL_PATTERN = /^\/GIT\/syntaxai\/tdd\.md\/(.+)$/; + +export const rewriteOldGitUrl = (pathname: string): string | null => { + const m = OLD_GIT_URL_PATTERN.exec(pathname); + if (m === null) return null; + return `/GIT/tdd.md/${m[1]}`; +}; diff --git a/src/b51_render_commit.ts b/src/b51_render_commit.ts index c655260950109dd6d5f1c1c73a26633564f9936e..4e95a472d47d907280bb69654812a9441fcc5305 100644 --- a/src/b51_render_commit.ts +++ b/src/b51_render_commit.ts @@ -89,7 +89,7 @@ export const renderCommitView = async (params: { const parentLinks = detail.parents.length === 0 ? `no parent (root commit)` : detail.parents.map((p) => - `${escape(shortSha(p))}`, + `${escape(shortSha(p))}`, ).join(" · "); const totalAdded = diff.files.reduce((s, f) => s + f.added, 0); @@ -113,7 +113,7 @@ export const renderCommitView = async (params: { ${filesSummary} ${diff.files.map(renderFile).join("")} `; diff --git a/src/b51_render_edit.ts b/src/b51_render_edit.ts index de56bd4b23fc1c4442dbd2c9fbbfa846839bfbb1..1e750aa9fd380578bd8aaab6558e4d130c13fca3 100644 --- a/src/b51_render_edit.ts +++ b/src/b51_render_edit.ts @@ -24,7 +24,7 @@ const shortSha = (sha: string): string => sha.slice(0, 7); // renders it through tdd.md's chrome — visitor never leaves the main // domain. const tddCommitUrl = (sha: string): string => - `/GIT/syntaxai/tdd.md/commit/${sha}`; + `/GIT/tdd.md/commit/${sha}`; // -------- /edit/:section/:slug — form for the admin -------- diff --git a/src/b51_render_repo.test.ts b/src/b51_render_repo.test.ts index f288376d926f6473f95d4982c13ab4f5eccfff92..8a855acf6424ea612d0e3c7782997f05bc448a0e 100644 --- a/src/b51_render_repo.test.ts +++ b/src/b51_render_repo.test.ts @@ -1,5 +1,5 @@ // Sibling test for c51_render_repo.ts (Layer 1, render). End-to-end -// shape covered by /GIT/syntaxai/tdd.md/tree|blob/main e2e specs. +// shape covered by /GIT/tdd.md/tree|blob/main e2e specs. // This pins the export surface. import { describe, test, expect } from "bun:test"; diff --git a/src/b51_render_repo.ts b/src/b51_render_repo.ts index 0da7be0c7fa54f4db27f2e40e7be69fd97db372c..3e51be4a0b43182f4c92da920b0206203b421dc1 100644 --- a/src/b51_render_repo.ts +++ b/src/b51_render_repo.ts @@ -1,5 +1,5 @@ // c51 — UI: tree listing + blob viewer for the local bare repo. -// Visited at /GIT/:owner/:repo/tree/:ref/ and /blob/:ref/. +// Visited at /GIT/:repo/tree/:ref/ and /blob/:ref/. // Renders through tdd.md's chrome (renderPage with bodyHtml). Markdown // blobs get parsed via marked; everything else is rendered as // preformatted source. @@ -20,8 +20,8 @@ const renderBreadcrumb = (params: { asBlob?: boolean; }): string => { const { owner, repo, ref, path, asBlob } = params; - const repoLink = `${escape(owner)}/${escape(repo)}`; - const refLink = `${escape(ref)}`; + const repoLink = `${escape(owner)}/${escape(repo)}`; + const refLink = `${escape(ref)}`; if (path === "") return `

${repoLink} · ${refLink}

`; const segments = path.split("/"); @@ -33,7 +33,7 @@ const renderBreadcrumb = (params: { // For tree view, every segment links to the tree at that depth. const isLastFile = asBlob && i === lastIdx; if (isLastFile) return `${escape(seg)}`; - return `${escape(seg)}`; + return `${escape(seg)}`; }) .join(" / "); return `

${repoLink} · ${refLink} · ${links}

`; @@ -62,7 +62,7 @@ const renderTreeRow = (params: { entry.type === "commit" ? "🔗" : // submodule "📄"; const kind = entry.type === "tree" ? "tree" : "blob"; - const href = `/GIT/${escape(owner)}/${escape(repo)}/${kind}/${escape(ref)}/${escape(childPath)}`; + const href = `/GIT/${escape(repo)}/${kind}/${escape(ref)}/${escape(childPath)}`; return ` ${icon} ${escape(entry.name)} @@ -84,8 +84,8 @@ export const renderRepoTree = async (params: { : (() => { const parentPath = path.includes("/") ? path.slice(0, path.lastIndexOf("/")) : ""; const upHref = parentPath === "" - ? `/GIT/${escape(owner)}/${escape(repo)}/tree/${escape(ref)}` - : `/GIT/${escape(owner)}/${escape(repo)}/tree/${escape(ref)}/${escape(parentPath)}`; + ? `/GIT/${escape(repo)}/tree/${escape(ref)}` + : `/GIT/${escape(repo)}/tree/${escape(ref)}/${escape(parentPath)}`; return `⬆..`; })(); const rows = entries.length === 0 @@ -135,8 +135,8 @@ export const renderRepoBlob = async (params: { ${escape(filename)} ${content.split("\n").length} lines · ${content.length} bytes - raw - ${isMarkdown(path) ? `· source` : ""} + raw + ${isMarkdown(path) ? `· source` : ""} ${bodyHtml} diff --git a/src/d21_app.ts b/src/d21_app.ts index e1f7dd0d2f7268560fec1c404a505b5f41c02fdb..63f41452397b700297ef6f603763a235d2f3fe7f 100644 --- a/src/d21_app.ts +++ b/src/d21_app.ts @@ -483,7 +483,7 @@ ${rows} // SAMA-native commit view — Bun-rendered alternative to Forgejo's // ///commit/ page. The :sha param may carry a // trailing ".diff" which the handler handles inline. - "/GIT/:owner/:repo/commit/:sha": commitViewHandler, + "/GIT/:repo/commit/:sha": commitViewHandler, "/auth/github/start": (req) => startGithubOauth(req), diff --git a/src/d21_handlers_commit_view.ts b/src/d21_handlers_commit_view.ts index cb8d80bae0554a69ec5b45e2a17a8bc037fb7a2b..ae8d0d98e4c8d2e662b33f1fb7ec1c096985897b 100644 --- a/src/d21_handlers_commit_view.ts +++ b/src/d21_handlers_commit_view.ts @@ -1,13 +1,13 @@ // c21 — handler: SAMA-native commit view at -// GET /GIT/:owner/:repo/commit/:sha +// GET /GIT/:repo/commit/:sha // and a raw-diff sibling at -// GET /GIT/:owner/:repo/commit/:sha.diff +// GET /GIT/:repo/commit/:sha.diff // // Composes c14 (Forgejo HTTP), c31 (diff parser), c51 (render). The // route prefix is uppercase /GIT/ to make it visually distinct from -// the markdown content sections (/sama, /blog, /guides). Visitors who -// land on git.tdd.md are bounced here by the deploy-time tunnel rule -// (out of scope for this handler — handler just owns the rendering). +// the markdown content sections (/sama, /blog, /guides). Owner is +// implicit (single-tenant — LIVE_REPO_OWNER) and never appears in +// the URL surface. import { renderNotFound, htmlResponse } from "./b51_render_layout.ts"; import { getCommit, getCommitDiff } from "./c14_git.ts"; @@ -15,38 +15,37 @@ import { LIVE_REPO_OWNER, LIVE_REPO_NAME } from "./a31_site_config.ts"; import { parseUnifiedDiff } from "./a31_diff_parse.ts"; import { renderCommitView } from "./b51_render_commit.ts"; -// Owner/repo + sha shape — paranoid because these go straight into a -// Forgejo URL. Owner/repo allow letters/digits/hyphens/underscores/dots; +// Repo + sha shape — paranoid because these go straight into a +// Forgejo URL. Repo allows letters/digits/hyphens/underscores/dots; // sha is hex 7-64 (Forgejo accepts shortened SHAs but our render assumes // full ones because we use them in URLs). const SAFE_OWNER_REPO = /^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/; const SAFE_SHA = /^[a-f0-9]{7,64}$/; -const isValid = (owner: string, repo: string, sha: string): boolean => - SAFE_OWNER_REPO.test(owner) && SAFE_OWNER_REPO.test(repo) && SAFE_SHA.test(sha); +const isValid = (repo: string, sha: string): boolean => + SAFE_OWNER_REPO.test(repo) && SAFE_SHA.test(sha); export const commitViewHandler = async ( - req: Request & { params: { owner: string; repo: string; sha: string } }, + req: Request & { params: { repo: string; sha: string } }, ): Promise => { - const { owner, repo } = req.params; + const { repo } = req.params; // The :sha param may carry a trailing ".diff" because the route // pattern doesn't have a separate one. Normalise + branch. const rawSha = req.params.sha; const wantsDiff = rawSha.endsWith(".diff"); const sha = wantsDiff ? rawSha.slice(0, -5) : rawSha; - const fullPath = `/GIT/${owner}/${repo}/commit/${rawSha}`; + const fullPath = `/GIT/${repo}/commit/${rawSha}`; - if (!isValid(owner, repo, sha)) { + if (!isValid(repo, sha)) { const html = await renderNotFound(fullPath); return htmlResponse(html, 404); } - // /GIT/ now serves only syntaxai/tdd.md (our local bare repo via - // c14_git). Other (owner, repo) pairs would historically have been - // proxied to Forgejo for agent katas — that's a separate concern - // and currently 404s. If we want it back, add a Forgejo fallback - // branch here keyed on the owner/repo pair. - if (owner !== LIVE_REPO_OWNER || repo !== LIVE_REPO_NAME) { + // /GIT/ now serves only the local bare repo (LIVE_REPO_NAME via + // c14_git). Other repos would historically have been proxied to + // Forgejo for agent katas — that's a separate concern and + // currently 404s. + if (repo !== LIVE_REPO_NAME) { const html = await renderNotFound(fullPath); return htmlResponse(html, 404); } @@ -85,6 +84,11 @@ export const commitViewHandler = async ( committerDate: commit.committerDate, message: commit.message, }; - const html = await renderCommitView({ owner, repo, detail, diff }); + const html = await renderCommitView({ + owner: LIVE_REPO_OWNER, + repo, + detail, + diff, + }); return htmlResponse(html); }; diff --git a/src/d21_handlers_fallback.ts b/src/d21_handlers_fallback.ts index 60b3a9e9831bc5836d25759d2d76aaa2753bf056..0246303a543f01c58569f47f8399b194652cbfb8 100644 --- a/src/d21_handlers_fallback.ts +++ b/src/d21_handlers_fallback.ts @@ -22,6 +22,7 @@ import { parseRepoBrowsePath, repoBrowseHandler, } from "./d21_handlers_repo_browse.ts"; +import { rewriteOldGitUrl } from "./b32_git_url_redirect.ts"; const isGitProtocol = (pathname: string, search: URLSearchParams): boolean => { if (pathname.includes(".git/") || pathname.endsWith(".git")) return true; @@ -122,22 +123,36 @@ export const appFetch = async (req: Request): Promise => { }); } - // SAMA-native repo browse at /GIT/:owner/:repo/{tree,blob,raw}/:ref/. + // Legacy /GIT/syntaxai/tdd.md/ URLs permanent-redirect to + // the new owner-less shape. MUST sit before the browse-match below + // so the legacy URL never reaches the browse handler. One regex + // covers every kind (tree/blob/raw/commit) + every future path. + const newGitPath = rewriteOldGitUrl(url.pathname); + if (newGitPath !== null) { + return new Response(null, { + status: 301, + headers: { + Location: newGitPath, + "Cache-Control": "public, max-age=86400", + }, + }); + } + + // SAMA-native repo browse at /GIT/:repo/{tree,blob,raw}/:ref/. // The wildcard path needs more flexibility than Bun's :param routes // give us (no slashes), so we match in the fallback fetch instead. const gitBrowseMatch = url.pathname.match( - /^\/GIT\/([A-Za-z0-9][A-Za-z0-9._-]+)\/([A-Za-z0-9][A-Za-z0-9._-]+)\/(.+)$/, + /^\/GIT\/([A-Za-z0-9][A-Za-z0-9._-]+)\/(.+)$/, ); if (gitBrowseMatch) { - const owner = gitBrowseMatch[1]!; - const repo = gitBrowseMatch[2]!; - const suffix = gitBrowseMatch[3]!; + const repo = gitBrowseMatch[1]!; + const suffix = gitBrowseMatch[2]!; // Skip the commit/ shape — that's c21_handlers_commit_view's // turf and lives as an explicit Bun.serve route in c21_app. if (!suffix.startsWith("commit/")) { const target = parseRepoBrowsePath(suffix); if (target !== null) { - return repoBrowseHandler(req, owner, repo, target); + return repoBrowseHandler(req, repo, target); } } } diff --git a/src/d21_handlers_repo_browse.ts b/src/d21_handlers_repo_browse.ts index 51e5a1be7eea9146e96c6f257bc412a032b826ea..2870f2162497e1584562463d0924a3ec45e7d603 100644 --- a/src/d21_handlers_repo_browse.ts +++ b/src/d21_handlers_repo_browse.ts @@ -23,11 +23,10 @@ const SAFE_OWNER_REPO = /^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/; // would clash with the wildcard path matching). const SAFE_REF = /^[A-Za-z0-9][A-Za-z0-9._-]{0,49}$/; -const isAllowedRepo = (owner: string, repo: string): boolean => - owner === LIVE_REPO_OWNER && - repo === LIVE_REPO_NAME && - SAFE_OWNER_REPO.test(owner) && - SAFE_OWNER_REPO.test(repo); +// Single-tenant: the only allowed repo is LIVE_REPO_NAME. Owner is +// implicit (LIVE_REPO_OWNER) and no longer carried in the URL. +const isAllowedRepo = (repo: string): boolean => + repo === LIVE_REPO_NAME && SAFE_OWNER_REPO.test(repo); // Only allow paths that look like ordinary repo entries — letters, // digits, hyphens, underscores, dots, slashes. Reject anything with @@ -68,13 +67,12 @@ export const parseRepoBrowsePath = (suffix: string): RepoBrowseTarget | null => export const repoBrowseHandler = async ( req: Request, - owner: string, repo: string, target: RepoBrowseTarget, ): Promise => { - const fullPath = `/GIT/${owner}/${repo}/${target.kind}/${target.ref}${target.path ? "/" + target.path : ""}`; + const fullPath = `/GIT/${repo}/${target.kind}/${target.ref}${target.path ? "/" + target.path : ""}`; - if (!isAllowedRepo(owner, repo)) { + if (!isAllowedRepo(repo)) { const html = await renderNotFound(fullPath); return htmlResponse(html, 404); } @@ -86,7 +84,7 @@ export const repoBrowseHandler = async ( return htmlResponse(html, 404); } const html = await renderRepoTree({ - owner, + owner: LIVE_REPO_OWNER, repo, ref: target.ref, path: target.path, @@ -102,7 +100,7 @@ export const repoBrowseHandler = async ( return htmlResponse(html, 404); } const html = await renderRepoBlob({ - owner, + owner: LIVE_REPO_OWNER, repo, ref: target.ref, path: target.path, diff --git a/src/d21_handlers_sama.ts b/src/d21_handlers_sama.ts index 97f3f6c6666bf636047477109410830c2d92310a..aa2383f9b1746ae781a45d8bf128aa42a77182c3 100644 --- a/src/d21_handlers_sama.ts +++ b/src/d21_handlers_sama.ts @@ -134,7 +134,7 @@ const renderV2Report = (report: SamaV2Report, metrics: SamaV2Metrics): string => > ${summary} -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. +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. | check | verdict | examined | |---|---|---|