eab00a378eb2bb8866ddcd30e2c097435415ffce diff --git a/content/blog/sama-v2-go-project-dive.md b/content/blog/sama-v2-go-project-dive.md new file mode 100644 index 0000000000000000000000000000000000000000..4fed28537d28413e87177065e5c007650fb523fa --- /dev/null +++ b/content/blog/sama-v2-go-project-dive.md @@ -0,0 +1,203 @@ +# Pointing SAMA v2 at `dive`: Go's conventions cover more than you'd think + +The [WordPress plugin audit](/blog/sama-v2-wordpress-plugin-audit) earlier today scored 0 of 7 §4 checks for a real-world plugin in the wild. That post argued — and I still believe — that the score isn't a failure; it's the expected baseline for code written under WordPress idioms with no external discipline. WP itself actively pushes devs toward hook-and-filter god-classes. + +But "WordPress is messy" isn't an interesting finding on its own. The harder question is what v2 sees when pointed at a language that has stronger architectural defaults built in. So: same exercise, same methodology, but against **`wagoodman/dive`** — a 53k-star Go project that explores Docker image layers. 8,498 lines of Go across 92 source files, downloaded straight from `git clone`, walked carefully. + +The result is much more interesting than 0/7. + +## What's in the box + +``` +dive/ +├── cmd/dive/ # CLI entry tree +│ ├── main.go # 12 lines: calls cli.Run() +│ └── cli/ +│ ├── cli.go # 130 lines: root command setup +│ └── internal/ +│ ├── options/ # 10 files, ~150 LOC each — YAML config types +│ ├── command/ # cobra command files (root, build, ci, export, adapter/) +│ └── ui/v1/ # the TUI: app/, view/, viewmodel/, layout/, key/ +├── dive/ # Domain tree +│ ├── filetree/ # 14 files: pure tree-diff logic. file_tree.go (390 LOC), +│ │ # file_node.go (354 LOC). Has 4 test files. +│ └── image/ # image archive parsing + Docker/Podman resolvers +│ ├── docker/ # 13 files: archive parsing, daemon API, CLI shelling +│ └── podman/ # 4 files: podman daemon + CLI +├── internal/ # Shared helpers +│ ├── utils/ ── 2 files +│ ├── log/ ── 1 file +│ └── bus/ ── 2 files + event/payload/ +└── (Dockerfile, Makefile, go.mod, etc.) +``` + +Some numbers worth flagging next to yesterday's WordPress plugin: + +| metric | `dive` (Go) | WP plugin (PHP) | +|---|---|---| +| Source files | 92 | 17 (non-vendor) | +| Total LOC | 8,498 | 6,445 | +| Largest single file | 496 LOC | **1,554 LOC** | +| Files over 700-LOC cap | **0** | 3 | +| Test files | 18 | 0 | +| Test LOC | 3,017 | 0 | +| Top-level layers as directories | `cmd/`, `internal/`, `dive/` | — none — | + +The two codebases are roughly the same size. They look completely different. + +## What `dive` already gets for free + +Go's standard project layout enforces several things that SAMA v2 has to write down as rules: + +- **`cmd/dive/main.go` is unambiguously the entry point.** Nothing imports back into it. Whatever lives in `cmd/`'s tree depends on `dive/` and `internal/`, not the other way around. That's the §1.2 Law direction enforced by Go's import resolver: `internal/` packages can only be imported by paths under their parent. The Law check has nothing left to do because the language already won't compile the violation. +- **Package-per-concern is a hard convention in Go.** `dive/filetree/` is the tree-diff package. `dive/image/docker/` is the Docker resolver. `dive/image/podman/` is Podman. There's no `Webdados_FB`-style god-class because the Go community would reject the PR at code review. Architecture as a property is half-enforced by the language ecosystem. +- **All files are under the 700-LOC cap.** The largest is `cmd/dive/cli/internal/ui/v1/view/filetree.go` at 496 lines; the next two are 472 and 390. Atomic check **passes by construction**. +- **Boundary parsing is mostly localized.** `json.Unmarshal`, `yaml.Unmarshal`, `os.Open`, `http.Client`, `exec.Command("docker", ...)` — almost every call appears under `dive/image/docker/` or `dive/image/podman/`. There's exactly one smell: `cmd/dive/cli/internal/options/ci.go` parses the user's `.dive-ci.yaml` config inside the `cmd/` tree. +- **Tests exist.** 18 test files, ~20% file ratio. Coverage is patchy (more on this below), but the WP plugin had zero. Modeled-tests goes from "vacuous fail" to "partial pass with named gaps." + +That covers maybe 60% of what v2 asks for, without anyone ever having looked at the spec. + +## What `dive` would still fail + +Walking the seven §4 checks honestly: + +### #1 Sorted — would fail + +> *Every file carries a profile-recognised prefix; lexicographic prefix order equals layer order.* + +This is the check Go projects cannot pass without changing language idioms. Go organizes by **directory**, not by **filename prefix**. Filenames inside `dive/filetree/` are `comparer.go`, `diff.go`, `efficiency.go`, `file_info.go`, `file_node.go`, `file_tree.go` — they're descriptive, not layer-marking. Lex-sorting them in alphabetical order has no relationship to architectural layer. + +This isn't `dive` doing something wrong. It's the SAMA v2 spec being written with a language model (TypeScript modules, PHP files) where prefix-sortable filenames are idiomatic. Go's idiom is the opposite. The spec needs a directory-based dialect for Go — *"the package directory's lex position equals the layer's order"* — to be honestly applicable here. I'll come back to this in the conclusion. + +### #2 Architecture — would partly pass + +> *Every file maps to exactly one canonical layer.* + +Without a written `sama.profile.toml` mapping packages to layers, every file is technically "unprefixed." But the **natural** mapping is obvious enough to write down right now: + +```toml +sama_version = "2.0" +profile = "dive" +layout = "directory" # ← hypothetical extension; see conclusion + +[layers.0] +packages = ["internal/utils", "internal/log", "internal/bus", "internal/bus/event/payload"] + +[layers.1] +sublayers = [ + { name = "core", packages = ["dive/filetree", "dive/image"] }, + { name = "viewmodel", packages = ["cmd/dive/cli/internal/ui/v1/viewmodel"] }, + { name = "view", packages = ["cmd/dive/cli/internal/ui/v1/view", "cmd/dive/cli/internal/ui/v1/layout", "cmd/dive/cli/internal/ui/v1/format", "cmd/dive/cli/internal/ui/v1/key"] }, +] + +[layers.2] +sublayers = [ + { name = "resolver", packages = ["dive/image/docker", "dive/image/podman"] }, + { name = "config", packages = ["cmd/dive/cli/internal/options"] }, +] + +[layers.3] +packages = ["cmd/dive", "cmd/dive/cli", "cmd/dive/cli/internal/command", "cmd/dive/cli/internal/command/ci", "cmd/dive/cli/internal/command/export", "cmd/dive/cli/internal/command/adapter", "cmd/dive/cli/internal/ui", "cmd/dive/cli/internal/ui/v1", "cmd/dive/cli/internal/ui/v1/app"] +``` + +That maps every package without ambiguity. Under the directory-based dialect, this passes. + +### #3 Modeled (tests) — would fail + +18 test files for 92 non-test source files. The packages with tests: + +- `dive/filetree/` — 4 tests (efficiency, file_node, file_tree, node_data) ✓ +- `cmd/dive/cli/internal/ui/v1/viewmodel/` — has tests ✓ +- `cmd/dive/cli/internal/command/ci/evaluator_test.go` ✓ +- `cmd/dive/cli/cli_test.go` + `cli_load_test.go` ✓ (integration-level) + +The packages without sibling tests include nearly all of `dive/image/`, `dive/image/docker/`, `dive/image/podman/` (the Layer 2 adapters), most of the UI view layer, and the entire `cmd/dive/cli/internal/command/` tree. That's ~30 source files in Layer 1 and Layer 2 territory that lack sibling tests. Modeled-tests would fail. + +### #4 Modeled (boundary) — would mostly pass + +Boundary parsing call sites: + +- `json.Unmarshal` / `json.NewDecoder` → 4 files in `dive/image/docker/`, all Layer 2. ✓ +- `yaml.Unmarshal` → `cmd/dive/cli/internal/options/ci.go` (Layer 2 config sublayer in the proposed profile) ✓ +- `os.Open` / `os.ReadFile` → `dive/filetree/file_info.go` (filesystem inspection, Layer 2 territory if you classify it that way) and `dive/image/docker/image_archive.go` (Layer 2) ✓ +- `exec.Command("docker", ...)` / `exec.Command("podman", ...)` → `dive/image/docker/cli.go` + `dive/image/podman/cli.go`, both Layer 2 ✓ +- `http.Client` → in the Docker daemon resolvers, Layer 2 ✓ + +One borderline case: `dive/filetree/file_info.go` calls `os.Lstat` and traverses the filesystem. Under v2's strict reading, anything filesystem-touching is Layer 2 (Adapter). But `filetree` is otherwise pure tree-diff math. Either split it (`filetree/` → Layer 1 + a new `dive/filetree_adapter/` → Layer 2), or accept that this one file's `Lstat` calls are tightly scoped. The check would still pass under the proposed profile because `file_info.go` is in the L1 `filetree` package — but it's a soft tension worth naming. + +Score: passes with one named tension. + +### #5 Atomic — passes outright + +All 92 source files under 700 LOC. No barrel files. Done. + +### #6 Law (§1.2) — would pass + +Go's `internal/` semantics + the natural cmd→domain→helpers direction mean upward imports are mostly impossible to write without the compiler refusing. The only same-layer-but-reversed-sublayer concern: does `dive/image/docker` import `dive/image/podman` or vice versa? A quick grep — neither imports the other. They're siblings under L2 resolver, both downstream of `dive/image/image.go` (which is L1 core). + +### #7 Consistency (§3) — would pass + +Derives from Law. No file's declared layer is contradicted by what it imports. + +**Estimated tally: 5 of 7 pass under the directory-based dialect, with 2 named failures (Sorted, Modeled-tests).** That's a real result, not "0/7 because no one tried." + +## The §5 metrics — estimated for `dive` + +| metric | `dive` (Go, estimated) | WP plugin (PHP, estimated) | tdd.md (TS, measured) | +|---|---|---|---| +| §4 checks passing | ~5 / 7 | 0 / 7 | 7 / 7 | +| graphDepth | ~5 (cmd → command → ui → dive → filetree → internal/utils) | ~3 | 7 | +| boundaryRatio | ~85% (one borderline case in `options/ci.go`) | <10% | 100% | +| workingSetFit (50–500 LOC) | ~80% | ~47% | 80% | +| violationCounts (sum) | ~30 (mostly Modeled-tests gaps) | 17+ | 0 | + +The `workingSetFit` is essentially **identical** between `dive` and this site (80%). Two unrelated projects, two different languages, two different scopes, written by different teams under different conventions — landing at the same fit ratio is a useful data point: 80% might just be what "reasonably engineered" looks like on this axis. + +## What `dive` would look like at 7/7 — the last 30% + +Far less work than the WordPress refactor sketch from earlier. Three concrete changes get from ~5/7 to 7/7: + +**1. Add `sama.profile.toml` (under a directory-based dialect).** The proposed profile in §2 above maps every package to a canonical layer. The dialect requires a spec extension (v2.1 maybe) — see conclusion. No code changes, just declaration. + +**2. Add sibling tests for the ~30 untested Layer-1/2 files.** This is the only real work. The candidates that most need them: + - `dive/image/docker/image_archive.go` (378 LOC, archive parsing — needs fixture-based tests) + - `dive/image/docker/engine_resolver.go` (197 LOC — needs a fake daemon) + - `dive/image/podman/build.go` (build coordination — needs a fake podman client) + - The `cmd/dive/cli/internal/command/ci/rules.go` rule-evaluation logic (196 LOC) + - The TUI `view/` files (496 + 377 + others) — testing renderers is annoying but feasible with text-fixture comparison + +A week of focused work would close this gap. Not a refactor — just tests being written that aren't there yet. + +**3. Resolve the `file_info.go` filesystem-vs-pure tension.** Split `dive/filetree/` so the pure tree math sits separately from the file-walking adapter. Concretely: move `file_info.go`'s `os.Lstat` calls into a new `dive/filetree_loader/` package, keep the tree algebra in `dive/filetree/`. ~half a day's work. + +The codebase is already so close to v2 that the lift is small. **Compare to the WordPress plugin where the same goal requires splitting a 1,554-line god-class into eleven files, writing 20+ test files from scratch, and introducing a typed Settings replacement for an untyped option array.** The "30% remaining" for `dive` is not a comparable amount of work. + +## Where this leaves the spec + +The audit surfaces one real finding about SAMA v2 itself: **the §4.1 Sorted check is written with TypeScript/PHP filename conventions in mind, and doesn't translate cleanly to Go**. + +Go's idiom is to organize by package directory, not by filename prefix. A `sama.profile.toml` that says *"these prefixes lex-sort in layer order"* (the v2.0 format) has nothing meaningful to assert about `dive`. A *directory-based* dialect — *"these package directories lex-sort in layer order"* — does. + +That's one of the §6 evolution-policy moves the spec was designed to accommodate: a new profile dialect, falsifiable against the §5 metrics, admitted only if measurements hold across multiple Go repos. Today's `dive` audit is the first datapoint for that hypothesis. To validate the directory-based dialect properly, the same exercise would need to be done against 3-5 more Go projects. That's a future post. + +## Three real-world datapoints, on the same axes + +After today, the §5 baseline graph has three points on it: + +| project | language | §4 score | workingSetFit | boundaryRatio | graphDepth | +|---|---|---|---|---|---| +| **tdd.md** (this site) | TypeScript | 7/7 (measured) | 80% (measured) | 100% (measured) | 7 (measured) | +| **wagoodman/dive** | Go | ~5/7 (estimated) | ~80% (estimated) | ~85% (estimated) | ~5 (estimated) | +| **Open Graph plugin** | PHP / WordPress | 0/7 (estimated) | ~47% (estimated) | <10% (estimated) | ~3 (estimated) | + +That's still n=3 and two of them are hand-estimated, so nobody should be drawing conclusions about empirical v2 worth yet. But the pattern is suggestive: real-world Go code, written under no v2 discipline, scores closer to the v2-disciplined dogfood than to the WP-idiom code. The differential is exactly the kind of thing §5 was designed to surface. Whether that differential causes better outcomes for agents working in the code — fewer harness loops, faster onboarding, less drift — is the next experiment, not this post's claim. + +--- + +**See for yourself:** + +- The project: +- Yesterday's WP audit (companion piece): [Pointing SAMA v2 at a real WordPress plugin in the wild](/blog/sama-v2-wordpress-plugin-audit) +- The hypothetical WP rebuild: [The Open Graph plugin, rebuilt under SAMA v2](/blog/sama-v2-wordpress-plugin-rebuilt) +- The §5 metric definitions: [/sama/v2#5-operational--core-metrics-definitions](/sama/v2#5-operational--core-metrics-definitions) +- The spec being audited against: [/sama/v2](/sama/v2) diff --git a/src/a31_blog.ts b/src/a31_blog.ts index 5fef265cacf6e548057779e4e857308985b1b147..0f2b1c5cd28c982eeec3b23ce91578e7a3313686 100644 --- a/src/a31_blog.ts +++ b/src/a31_blog.ts @@ -12,6 +12,12 @@ export interface BlogEntry { } export const ALL_POSTS: BlogEntry[] = [ + { + slug: "sama-v2-go-project-dive", + title: "Pointing SAMA v2 at `dive`: Go's conventions cover more than you'd think", + description: "After auditing a WordPress plugin (0/7) and sketching its v2-rebuild, the natural follow-up was a Go project for n=3. Picked wagoodman/dive — 53k-star Docker image explorer, 8,498 LOC across 92 source files, mature codebase. Cloned, walked the source, scored honestly. The result is much more interesting than 0/7: Go's standard layout (cmd/, internal/, package-per-concern) plus the language's internal/ semantics already enforce the §1.2 Law, the Atomic 700-LOC cap (largest file 496), and Architecture-by-package — five of the seven §4 checks pass naturally. The two that don't: #1 Sorted (Go organizes by package directory, not filename prefix — incompatible with v2.0's lex-sort-the-prefixes rule) and #3 Modeled-tests (18 test files for 92 source files, gaps in the image adapters). The audit surfaces a real spec-evolution finding: v2 needs a directory-based dialect for Go, where 'package directory lex-position = layer order' replaces 'filename prefix lex-position = layer order'. The 30% remaining work to push dive to 7/7 is far smaller than the WP plugin's: add a profile under the new dialect, write ~30 sibling tests for image-adapter files, split filetree/ into pure + loader. Three datapoints now on the §5 axes: tdd.md (TS, 7/7, fit 80%), dive (Go, ~5/7, fit ~80%), WP plugin (PHP, 0/7, fit ~47%). The pattern is suggestive — real Go scores closer to v2-disciplined code than to WP-idiom code — but n=3 with two hand-estimates is not yet a worth-following claim.", + date: "2026-05-23", + }, { slug: "sama-v2-wordpress-plugin-rebuilt", title: "The Open Graph plugin, rebuilt under SAMA v2 — a thought experiment",