| 1 | +# Pointing SAMA v2 at `dive`: Go's conventions cover more than you'd think |
| 2 | + |
| 3 | +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. |
| 4 | + |
| 5 | +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. |
| 6 | + |
| 7 | +The result is much more interesting than 0/7. |
| 8 | + |
| 9 | +## What's in the box |
| 10 | + |
| 11 | +``` |
| 12 | +dive/ |
| 13 | +├── cmd/dive/ # CLI entry tree |
| 14 | +│ ├── main.go # 12 lines: calls cli.Run() |
| 15 | +│ └── cli/ |
| 16 | +│ ├── cli.go # 130 lines: root command setup |
| 17 | +│ └── internal/ |
| 18 | +│ ├── options/ # 10 files, ~150 LOC each — YAML config types |
| 19 | +│ ├── command/ # cobra command files (root, build, ci, export, adapter/) |
| 20 | +│ └── ui/v1/ # the TUI: app/, view/, viewmodel/, layout/, key/ |
| 21 | +├── dive/ # Domain tree |
| 22 | +│ ├── filetree/ # 14 files: pure tree-diff logic. file_tree.go (390 LOC), |
| 23 | +│ │ # file_node.go (354 LOC). Has 4 test files. |
| 24 | +│ └── image/ # image archive parsing + Docker/Podman resolvers |
| 25 | +│ ├── docker/ # 13 files: archive parsing, daemon API, CLI shelling |
| 26 | +│ └── podman/ # 4 files: podman daemon + CLI |
| 27 | +├── internal/ # Shared helpers |
| 28 | +│ ├── utils/ ── 2 files |
| 29 | +│ ├── log/ ── 1 file |
| 30 | +│ └── bus/ ── 2 files + event/payload/ |
| 31 | +└── (Dockerfile, Makefile, go.mod, etc.) |
| 32 | +``` |
| 33 | + |
| 34 | +Some numbers worth flagging next to yesterday's WordPress plugin: |
| 35 | + |
| 36 | +| metric | `dive` (Go) | WP plugin (PHP) | |
| 37 | +|---|---|---| |
| 38 | +| Source files | 92 | 17 (non-vendor) | |
| 39 | +| Total LOC | 8,498 | 6,445 | |
| 40 | +| Largest single file | 496 LOC | **1,554 LOC** | |
| 41 | +| Files over 700-LOC cap | **0** | 3 | |
| 42 | +| Test files | 18 | 0 | |
| 43 | +| Test LOC | 3,017 | 0 | |
| 44 | +| Top-level layers as directories | `cmd/`, `internal/`, `dive/` | — none — | |
| 45 | + |
| 46 | +The two codebases are roughly the same size. They look completely different. |
| 47 | + |
| 48 | +## What `dive` already gets for free |
| 49 | + |
| 50 | +Go's standard project layout enforces several things that SAMA v2 has to write down as rules: |
| 51 | + |
| 52 | +- **`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. |
| 53 | +- **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. |
| 54 | +- **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**. |
| 55 | +- **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. |
| 56 | +- **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." |
| 57 | + |
| 58 | +That covers maybe 60% of what v2 asks for, without anyone ever having looked at the spec. |
| 59 | + |
| 60 | +## What `dive` would still fail |
| 61 | + |
| 62 | +Walking the seven §4 checks honestly: |
| 63 | + |
| 64 | +### #1 Sorted — would fail |
| 65 | + |
| 66 | +> *Every file carries a profile-recognised prefix; lexicographic prefix order equals layer order.* |
| 67 | + |
| 68 | +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. |
| 69 | + |
| 70 | +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. |
| 71 | + |
| 72 | +### #2 Architecture — would partly pass |
| 73 | + |
| 74 | +> *Every file maps to exactly one canonical layer.* |
| 75 | + |
| 76 | +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: |
| 77 | + |
| 78 | +```toml |
| 79 | +sama_version = "2.0" |
| 80 | +profile = "dive" |
| 81 | +layout = "directory" # ← hypothetical extension; see conclusion |
| 82 | + |
| 83 | +[layers.0] |
| 84 | +packages = ["internal/utils", "internal/log", "internal/bus", "internal/bus/event/payload"] |
| 85 | + |
| 86 | +[layers.1] |
| 87 | +sublayers = [ |
| 88 | + { name = "core", packages = ["dive/filetree", "dive/image"] }, |
| 89 | + { name = "viewmodel", packages = ["cmd/dive/cli/internal/ui/v1/viewmodel"] }, |
| 90 | + { 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"] }, |
| 91 | +] |
| 92 | + |
| 93 | +[layers.2] |
| 94 | +sublayers = [ |
| 95 | + { name = "resolver", packages = ["dive/image/docker", "dive/image/podman"] }, |
| 96 | + { name = "config", packages = ["cmd/dive/cli/internal/options"] }, |
| 97 | +] |
| 98 | + |
| 99 | +[layers.3] |
| 100 | +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"] |
| 101 | +``` |
| 102 | + |
| 103 | +That maps every package without ambiguity. Under the directory-based dialect, this passes. |
| 104 | + |
| 105 | +### #3 Modeled (tests) — would fail |
| 106 | + |
| 107 | +18 test files for 92 non-test source files. The packages with tests: |
| 108 | + |
| 109 | +- `dive/filetree/` — 4 tests (efficiency, file_node, file_tree, node_data) ✓ |
| 110 | +- `cmd/dive/cli/internal/ui/v1/viewmodel/` — has tests ✓ |
| 111 | +- `cmd/dive/cli/internal/command/ci/evaluator_test.go` ✓ |
| 112 | +- `cmd/dive/cli/cli_test.go` + `cli_load_test.go` ✓ (integration-level) |
| 113 | + |
| 114 | +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. |
| 115 | + |
| 116 | +### #4 Modeled (boundary) — would mostly pass |
| 117 | + |
| 118 | +Boundary parsing call sites: |
| 119 | + |
| 120 | +- `json.Unmarshal` / `json.NewDecoder` → 4 files in `dive/image/docker/`, all Layer 2. ✓ |
| 121 | +- `yaml.Unmarshal` → `cmd/dive/cli/internal/options/ci.go` (Layer 2 config sublayer in the proposed profile) ✓ |
| 122 | +- `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) ✓ |
| 123 | +- `exec.Command("docker", ...)` / `exec.Command("podman", ...)` → `dive/image/docker/cli.go` + `dive/image/podman/cli.go`, both Layer 2 ✓ |
| 124 | +- `http.Client` → in the Docker daemon resolvers, Layer 2 ✓ |
| 125 | + |
| 126 | +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. |
| 127 | + |
| 128 | +Score: passes with one named tension. |
| 129 | + |
| 130 | +### #5 Atomic — passes outright |
| 131 | + |
| 132 | +All 92 source files under 700 LOC. No barrel files. Done. |
| 133 | + |
| 134 | +### #6 Law (§1.2) — would pass |
| 135 | + |
| 136 | +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). |
| 137 | + |
| 138 | +### #7 Consistency (§3) — would pass |
| 139 | + |
| 140 | +Derives from Law. No file's declared layer is contradicted by what it imports. |
| 141 | + |
| 142 | +**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." |
| 143 | + |
| 144 | +## The §5 metrics — estimated for `dive` |
| 145 | + |
| 146 | +| metric | `dive` (Go, estimated) | WP plugin (PHP, estimated) | tdd.md (TS, measured) | |
| 147 | +|---|---|---|---| |
| 148 | +| §4 checks passing | ~5 / 7 | 0 / 7 | 7 / 7 | |
| 149 | +| graphDepth | ~5 (cmd → command → ui → dive → filetree → internal/utils) | ~3 | 7 | |
| 150 | +| boundaryRatio | ~85% (one borderline case in `options/ci.go`) | <10% | 100% | |
| 151 | +| workingSetFit (50–500 LOC) | ~80% | ~47% | 80% | |
| 152 | +| violationCounts (sum) | ~30 (mostly Modeled-tests gaps) | 17+ | 0 | |
| 153 | + |
| 154 | +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. |
| 155 | + |
| 156 | +## What `dive` would look like at 7/7 — the last 30% |
| 157 | + |
| 158 | +Far less work than the WordPress refactor sketch from earlier. Three concrete changes get from ~5/7 to 7/7: |
| 159 | + |
| 160 | +**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. |
| 161 | + |
| 162 | +**2. Add sibling tests for the ~30 untested Layer-1/2 files.** This is the only real work. The candidates that most need them: |
| 163 | + - `dive/image/docker/image_archive.go` (378 LOC, archive parsing — needs fixture-based tests) |
| 164 | + - `dive/image/docker/engine_resolver.go` (197 LOC — needs a fake daemon) |
| 165 | + - `dive/image/podman/build.go` (build coordination — needs a fake podman client) |
| 166 | + - The `cmd/dive/cli/internal/command/ci/rules.go` rule-evaluation logic (196 LOC) |
| 167 | + - The TUI `view/` files (496 + 377 + others) — testing renderers is annoying but feasible with text-fixture comparison |
| 168 | + |
| 169 | +A week of focused work would close this gap. Not a refactor — just tests being written that aren't there yet. |
| 170 | + |
| 171 | +**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. |
| 172 | + |
| 173 | +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. |
| 174 | + |
| 175 | +## Where this leaves the spec |
| 176 | + |
| 177 | +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**. |
| 178 | + |
| 179 | +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. |
| 180 | + |
| 181 | +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. |
| 182 | + |
| 183 | +## Three real-world datapoints, on the same axes |
| 184 | + |
| 185 | +After today, the §5 baseline graph has three points on it: |
| 186 | + |
| 187 | +| project | language | §4 score | workingSetFit | boundaryRatio | graphDepth | |
| 188 | +|---|---|---|---|---|---| |
| 189 | +| **tdd.md** (this site) | TypeScript | 7/7 (measured) | 80% (measured) | 100% (measured) | 7 (measured) | |
| 190 | +| **wagoodman/dive** | Go | ~5/7 (estimated) | ~80% (estimated) | ~85% (estimated) | ~5 (estimated) | |
| 191 | +| **Open Graph plugin** | PHP / WordPress | 0/7 (estimated) | ~47% (estimated) | <10% (estimated) | ~3 (estimated) | |
| 192 | + |
| 193 | +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. |
| 194 | + |
| 195 | +--- |
| 196 | + |
| 197 | +**See for yourself:** |
| 198 | + |
| 199 | +- The project: <https://github.com/wagoodman/dive> |
| 200 | +- Yesterday's WP audit (companion piece): [Pointing SAMA v2 at a real WordPress plugin in the wild](/blog/sama-v2-wordpress-plugin-audit) |
| 201 | +- The hypothetical WP rebuild: [The Open Graph plugin, rebuilt under SAMA v2](/blog/sama-v2-wordpress-plugin-rebuilt) |
| 202 | +- The §5 metric definitions: [/sama/v2#5-operational--core-metrics-definitions](/sama/v2#5-operational--core-metrics-definitions) |
| 203 | +- The spec being audited against: [/sama/v2](/sama/v2) |