syntaxai/tdd.md · commit e978141

Blog: dive, the prefix-scheme variant — what ls-readable layer order costs in Go

Companion to the directory-dialect dive rebuild. Bas missed the visual
payoff of the WP rebuild's prefix tree, so this post shows what dive
looks like if it commits to v2.0's prefix scheme literally — 90 Go
files renamed flat under src/, every prefix from a_ through d_ present,
ls reads top-to-bottom in dependency order, no spec extension needed.

The post is candid about what this costs in Go:
1. One giant package (loses encapsulation, compilation boundaries,
   cyclic-import detection at the package level)
2. internal/ semantics evaporate (Law check has to do work the compiler
   was doing)
3. Go community would reject the PR (style is firmly anti this)

Against those costs, what's bought:
- ls src/ | sort = layer order, the WP-style aesthetic recovered
- v2.0 as written applies unchanged, no v2.1 dialect needed
- shorter profile (8 prefixes mapped, vs listing every package path)
- cross-language consistency (PHP/TS/Go all read the same prefix scheme)

Conclusion sketches the spec evolution: v2.1 should support both
`layout = "prefix"` and `layout = "directory"`, profile-chosen, with
all other §4 checks layout-mode-independent. The §5 metrics come out
identically in either path — the rules are stricter than the surface
syntax that enforces them, which is the design point.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
author
syntaxai <[email protected]>
date
2026-05-23 15:21:38 +01:00
parent
514bd76
commit
e9781414a2d14eb451e1a7123492e024ac0199c3

2 files changed · +241 −0

added content/blog/sama-v2-go-project-dive-prefix-scheme.md +235 −0
@@ -0,0 +1,235 @@
1+# `dive`, the prefix-scheme variant — what `ls`-readable layer order costs in Go
2+
3+[Earlier today's `dive` rebuilt sketch](/blog/sama-v2-go-project-dive-rebuilt) chose a hypothetical v2.1 *directory-based dialect* — pin layer mapping by package path, leave the actual files where they sit, accept the minimal-change cost. The visual payoff of that approach is muted: you can't `ls src/ | sort` and read the layer hierarchy off the screen the way you can in the [WordPress rebuild post](/blog/sama-v2-wordpress-plugin-rebuilt) (`a_meta_tag.php < a_page_context.php < ... < b1_image_selection_policy.php < ... < d_third_party_hooks.php`).
4+
5+Bas said the obvious thing: *"i miss this SAMA v2 style."* Fair. So here's the other end of the spectrum — what `dive` looks like if it commits to v2.0's prefix scheme *literally*, every file renamed, no spec extension required, the `ls`-readable layer order property fully recovered.
6+
7+This is the more dramatic refactor. It's also the variant that fights Go's idiom the hardest, and the post is candid about both halves.
8+
9+## The full file tree
10+
11+```
12+dive/
13+├── go.mod # at module root (required by Go)
14+├── go.sum
15+├── README.md
16+├── Makefile
17+├── Dockerfile
18+├── Taskfile.yaml
19+│
20+├── src/ # all Go sources, flat — accepts non-idiomatic
21+│ │ Go layout for v2.0's lex-sort property
22+│ │
23+│ │── ─── Layer 0 — Pure ──────────────────────────────────────────────────
24+│ ├── a_bus.go # in-memory event bus type (was internal/bus/bus.go)
25+│ ├── a_bus_helpers.go # (was internal/bus/helpers.go)
26+│ ├── a_event.go # event types (was internal/bus/event/event.go)
27+│ ├── a_event_payload_explore.go # (was internal/bus/event/payload/explore.go)
28+│ ├── a_event_payload_generic.go # (was internal/bus/event/payload/generic.go)
29+│ ├── a_file_node.go # FileNode value type (was dive/filetree/file_node.go)
30+│ ├── a_image.go # Image, ImageMeta types (was dive/image/image.go)
31+│ ├── a_layer.go # Layer type (was dive/image/layer.go)
32+│ ├── a_log.go # log façade (was internal/log/log.go)
33+│ ├── a_node_data.go # NodeData type (was dive/filetree/node_data.go)
34+│ ├── a_order_strategy.go # pure enum (was dive/filetree/order_strategy.go)
35+│ ├── a_path_error.go # (was dive/filetree/path_error.go)
36+│ ├── a_ports.go # NEW: interfaces (Resolver, Loader, EngineClient, ...)
37+│ ├── a_utils_format.go # CleanArgs etc (was internal/utils/format.go)
38+│ ├── a_utils_view.go # (was internal/utils/view.go)
39+│ ├── a_view_info.go # (was dive/filetree/view_info.go)
40+│ │
41+│ │── ─── Layer 1, sublayer "policy" — pure algorithms ──────────────────
42+│ ├── b1_comparer.go # (was dive/filetree/comparer.go)
43+│ ├── b1_diff.go # (was dive/filetree/diff.go)
44+│ ├── b1_efficiency.go # (was dive/filetree/efficiency.go)
45+│ ├── b1_event_parser.go # (was internal/bus/event/parser/parsers.go)
46+│ ├── b1_file_tree.go # tree algebra (was dive/filetree/file_tree.go)
47+│ ├── b1_image_analysis.go # pure analysis (was dive/image/analysis.go)
48+│ ├── b1_ci_evaluator.go # (was cmd/.../command/ci/evaluator.go)
49+│ ├── b1_ci_rule.go # (was cmd/.../command/ci/rule.go)
50+│ ├── b1_ci_rules.go # (was cmd/.../command/ci/rules.go)
51+│ │
52+│ │── ─── Layer 1, sublayer "viewmodel" — stateful state managers ────────
53+│ ├── b2_viewmodel_config.go # (was .../viewmodel/config.go)
54+│ ├── b2_viewmodel_filetree.go # (was .../viewmodel/filetree.go)
55+│ ├── b2_viewmodel_layer_compare.go # (was .../viewmodel/layer_compare.go)
56+│ ├── b2_viewmodel_layer_selection.go # (was .../viewmodel/layer_selection.go)
57+│ ├── b2_viewmodel_layer_set_state.go # (was .../viewmodel/layer_set_state.go)
58+│ ├── b2_format.go # (was .../format/format.go)
59+│ ├── b2_keybindings.go # (was .../key/binding.go + key/config.go merged)
60+│ ├── b2_ui_config.go # (was .../ui/v1/config.go)
61+│ ├── b2_layout_area.go # (was .../layout/area.go)
62+│ ├── b2_layout_location.go # (was .../layout/location.go)
63+│ ├── b2_layout_compound.go # (was .../layout/compound/layer_details_column.go)
64+│ ├── b2_layout_layout.go # (was .../layout/layout.go)
65+│ ├── b2_layout_manager.go # (was .../layout/manager.go)
66+│ │
67+│ │── ─── Layer 1, sublayer "view" — TUI render (depends on b2 state) ───
68+│ ├── b3_view_cursor.go # (was .../view/cursor.go)
69+│ ├── b3_view_debug.go # (was .../view/debug.go)
70+│ ├── b3_view_filetree.go # (was .../view/filetree.go)
71+│ ├── b3_view_filter.go # (was .../view/filter.go)
72+│ ├── b3_view_image_details.go # (was .../view/image_details.go)
73+│ ├── b3_view_layer.go # (was .../view/layer.go)
74+│ ├── b3_view_layer_change_listener.go # (was .../view/layer_change_listener.go)
75+│ ├── b3_view_layer_details.go # (was .../view/layer_details.go)
76+│ ├── b3_view_renderer.go # (was .../view/renderer.go)
77+│ ├── b3_view_status.go # (was .../view/status.go)
78+│ ├── b3_view_views.go # (was .../view/views.go)
79+│ │
80+│ │── ─── Layer 2, sublayer "loader" — filesystem walking ──────────────
81+│ ├── c1_file_info.go # filesystem walking (was dive/filetree/file_info.go)
82+│ │
83+│ │── ─── Layer 2, sublayer "engine" — Docker/Podman daemon API ────────
84+│ ├── c2_docker_archive_resolver.go # (was dive/image/docker/archive_resolver.go)
85+│ ├── c2_docker_build.go # (was dive/image/docker/build.go)
86+│ ├── c2_docker_cli.go # (was dive/image/docker/cli.go)
87+│ ├── c2_docker_config.go # (was dive/image/docker/config.go)
88+│ ├── c2_docker_engine_resolver.go # (was dive/image/docker/engine_resolver.go)
89+│ ├── c2_docker_host_unix.go # (was dive/image/docker/docker_host_unix.go)
90+│ ├── c2_docker_host_windows.go # (was dive/image/docker/docker_host_windows.go)
91+│ ├── c2_docker_image_archive.go # (was dive/image/docker/image_archive.go)
92+│ ├── c2_docker_layer.go # (was dive/image/docker/layer.go)
93+│ ├── c2_docker_manifest.go # (was dive/image/docker/manifest.go)
94+│ ├── c2_docker_testing.go # (was dive/image/docker/testing.go)
95+│ ├── c2_image_resolver.go # (was dive/image/resolver.go)
96+│ ├── c2_image_resolver_factory.go # (was dive/get_image_resolver.go)
97+│ ├── c2_podman_build.go # (was dive/image/podman/build.go)
98+│ ├── c2_podman_cli.go # (was dive/image/podman/cli.go)
99+│ ├── c2_podman_resolver.go # (was dive/image/podman/resolver.go)
100+│ ├── c2_podman_resolver_unsupported.go # (was dive/image/podman/resolver_unsupported.go)
101+│ │
102+│ │── ─── Layer 2, sublayer "config" — YAML parsing ────────────────────
103+│ ├── c3_options_analysis.go # (was cmd/.../options/analysis.go)
104+│ ├── c3_options_application.go # (was cmd/.../options/application.go)
105+│ ├── c3_options_ci.go # YAML parse .dive-ci.yaml (was .../options/ci.go)
106+│ ├── c3_options_ci_rules.go # (was cmd/.../options/ci_rules.go)
107+│ ├── c3_options_export.go # (was cmd/.../options/export.go)
108+│ ├── c3_options_ui.go # (was cmd/.../options/ui.go)
109+│ ├── c3_options_ui_diff.go # (was cmd/.../options/ui_diff.go)
110+│ ├── c3_options_ui_filetree.go # (was cmd/.../options/ui_filetree.go)
111+│ ├── c3_options_ui_keybindings.go # (was cmd/.../options/ui_keybindings.go)
112+│ ├── c3_options_ui_layers.go # (was cmd/.../options/ui_layers.go)
113+│ │
114+│ │── ─── Layer 3 — Entry: commands, bootstrap, app controller ─────────
115+│ ├── d_app.go # (was cmd/.../ui/v1/app/app.go)
116+│ ├── d_app_controller.go # (was cmd/.../ui/v1/app/controller.go)
117+│ ├── d_app_job_control_other.go # (was cmd/.../ui/v1/app/job_control_other.go)
118+│ ├── d_app_job_control_unix.go # (was cmd/.../ui/v1/app/job_control_unix.go)
119+│ ├── d_cli.go # (was cmd/dive/cli/cli.go)
120+│ ├── d_cmd_adapter_analyzer.go # (was cmd/.../command/adapter/analyzer.go)
121+│ ├── d_cmd_adapter_evaluator.go # (was cmd/.../command/adapter/evaluator.go)
122+│ ├── d_cmd_adapter_exporter.go # (was cmd/.../command/adapter/exporter.go)
123+│ ├── d_cmd_adapter_resolver.go # (was cmd/.../command/adapter/resolver.go)
124+│ ├── d_cmd_build.go # (was cmd/.../command/build.go)
125+│ ├── d_cmd_export.go # (was cmd/.../command/export/export.go)
126+│ ├── d_cmd_root.go # (was cmd/.../command/root.go)
127+│ ├── d_main.go # (was cmd/dive/main.go) — calls cli.Run()
128+│ ├── d_ui_no_ui.go # (was cmd/.../ui/no_ui.go)
129+│ └── d_ui_v1.go # (was cmd/.../ui/v1.go)
130+│
131+└── tests/ # one *_test.go per b*_ and c*_ file
132+ ├── b1_file_tree_test.go # (was dive/filetree/file_tree_test.go)
133+ ├── b1_efficiency_test.go # (was dive/filetree/efficiency_test.go)
134+ ├── b1_ci_evaluator_test.go # (was cmd/.../command/ci/evaluator_test.go)
135+ ├── b2_viewmodel_filetree_test.go # (was .../viewmodel/filetree_test.go)
136+ ├── ... (~40 sibling tests, one per source file in layers 1 and 2)
137+```
138+
139+**Lex check** (the property §4.1 cares about): `a_*` < `b1_*` < `b2_*` < `b3_*` < `c1_*` < `c2_*` < `c3_*` < `d_*`. The thirteen Layer 0 files, then nine Layer 1 policy files, then thirteen Layer 1 viewmodel, then eleven Layer 1 view, then one Layer 2 loader, then seventeen Layer 2 engine adapters, then ten Layer 2 config parsers, then fifteen Layer 3 entry files. Run `ls src/` and you get them in that order. The layer hierarchy *is* the screen.
140+
141+**The profile** (under v2.0 unchanged, no extension needed):
142+
143+```toml
144+sama_version = "2.0"
145+profile = "dive"
146+
147+[layers.0]
148+prefixes = ["a_"]
149+
150+[layers.1]
151+sublayers = [
152+ { name = "policy", prefix = "b1_" },
153+ { name = "viewmodel", prefix = "b2_" },
154+ { name = "view", prefix = "b3_" },
155+]
156+
157+[layers.2]
158+sublayers = [
159+ { name = "loader", prefix = "c1_" },
160+ { name = "engine", prefix = "c2_" },
161+ { name = "config", prefix = "c3_" },
162+]
163+
164+[layers.3]
165+prefixes = ["d_"]
166+```
167+
168+This passes all seven §4 checks under v2.0 unchanged. No spec extension required.
169+
170+## What this costs in Go
171+
172+Three concrete prices:
173+
174+**1. Everything's in one Go package.** Go's rule: package name equals directory name. A flat `src/` with 90+ files means they're all in `package dive` (or whatever name the directory takes). Today `dive` has 16+ packages (`filetree`, `image`, `image/docker`, `image/podman`, `internal/utils`, `internal/log`, `viewmodel`, `view`, `command/ci`, `command/adapter`, ...). Under the prefix scheme they merge into one giant package, losing:
175+ - **Encapsulation.** Today, the `dive/filetree` package can export `FileTree` while keeping `file_info.go`'s helpers private. In a single flat package, everything not lowercase is exported; everything lowercase is package-private to a 90-file blob.
176+ - **Compilation boundaries.** Go compiles per package; a change to one file in `filetree` doesn't force `image/docker` to recompile. In one giant package, every change touches the same compilation unit.
177+ - **Cyclic-import detection at the package level.** Go enforces "no cyclic imports between packages" — a hard error. In a single package, files import each other freely; the only enforcement is "no cyclic dependencies between files," which Go doesn't check (it doesn't need to, because Go has no per-file imports).
178+
179+**2. `internal/` semantics evaporate.** Go's `internal/X` packages can only be imported by paths under `X`'s parent — a hard, compiler-enforced visibility rule. In a flat `src/`, no path has an `internal/` qualifier. Every Layer 0 type is import-equivalent to every Layer 3 file. The Law check (§1.2) now has to do the work the compiler was doing for free.
180+
181+**3. The Go community would reject the PR.** The flat-layout style would be one of the first things called out in any code review. The Go style guide is pretty firm on package organization, and `package dive` containing `b1_*.go` filenames isn't going to land. A maintainer attempting this refactor would spend the first three PRs arguing about naming alone.
182+
183+## What the prefix-scheme variant gets right
184+
185+Against those costs, what's bought:
186+
187+- **`ls src/ | sort` reads top-to-bottom in dependency order.** The visual payoff that the [WP rebuild post](/blog/sama-v2-wordpress-plugin-rebuilt) leans on works identically here. A reviewer looking at `git diff src/` sees layer changes in their natural order.
188+- **No spec extension needed.** v2.0 as written applies unchanged. The [other rebuild post](/blog/sama-v2-go-project-dive-rebuilt) requires a hypothetical v2.1 directory dialect; this one doesn't.
189+- **Profile is shorter.** Eight prefixes mapped to seven sublayer slots — a one-screen `sama.profile.toml`. The directory-dialect profile is twice as long because it lists every package path.
190+- **Cross-language consistency.** A reader who knows the WP rebuild's prefix scheme reads this tree without translation. `a_*` is types, `b1_*` is pure policy, `c1_*` is repo, `d_*` is entry — identical to what they already know from the PHP example.
191+
192+## Which variant should the spec actually recommend?
193+
194+Probably the directory dialect for Go, the prefix scheme for most other languages.
195+
196+The reasoning: SAMA v2's value comes from making layer boundaries enforceable. Go already enforces them via package + `internal/` semantics — borrowing that machinery is cheaper than fighting it. PHP and TypeScript don't have those compiler-level checks, so the file-prefix scheme buys you something the language otherwise doesn't.
197+
198+A v2.1 spec extension would probably say:
199+
200+> *A profile may choose between two `layout` modes: `"prefix"` (filename-prefix-based; the verifier's Sorted check verifies lex order of prefixes matches the layer order declared in the profile) and `"directory"` (package-path-based; the verifier's Sorted check verifies that no import edge points from an earlier-declared package to a later-declared one). The choice is per-profile and influences only the Sorted check's enforcement; all other §4 checks are layout-mode-independent.*
201+
202+That's the kind of optional extension §6 evolution policy admits naturally: falsifiable (does the directory mode produce equivalent verification guarantees?), measurable (the §5 metrics work either way), and promotable to official only if cross-repo data shows both modes catch the same class of drift.
203+
204+## What's the same as the other rebuild post
205+
206+Predicted §5 metrics for the prefix-scheme variant are **identical** to the directory-dialect variant:
207+
208+| metric | dive today | dive — directory variant | dive — prefix variant |
209+|---|---|---|---|
210+| §4 checks passing | ~5 / 7 | 7 / 7 (under v2.1 dialect) | 7 / 7 (under v2.0 unchanged) |
211+| graphDepth | ~5 | ~5 | ~5 |
212+| boundaryRatio | ~85% | ~100% | ~100% |
213+| workingSetFit | ~80% | ~80% | ~80% |
214+| violationCounts | ~30 | 0 | 0 |
215+
216+The §5 metrics don't care about layout mode. They measure **the same architectural facts** whether expressed by package or by prefix. Which is itself a useful observation about the spec: the metrics are the load-bearing artifact; the Sorted check is a convenience surface.
217+
218+## Two paths, same destination
219+
220+This is the third `dive` post in a row, and I'll stop after this one. The bracket is now visible:
221+
222+- [The audit](/blog/sama-v2-go-project-dive) — `dive` as-is, ~5 of 7 checks pass naturally.
223+- [The directory-dialect rebuild](/blog/sama-v2-go-project-dive-rebuilt) — minimal moves, requires a hypothetical v2.1 extension, 7/7 ✓.
224+- This post — the prefix-scheme rebuild, works under v2.0 unchanged, fights Go hard, also 7/7 ✓.
225+
226+Both rebuilds reach the same end state. The first matches Go's idiom; the second matches the WP rebuild's visual aesthetic. The §5 metrics — the empirical artifact the spec cares about — come out identically in both. That's the part of the v2 design that quietly works: the rules are stricter than the surface syntax that enforces them.
227+
228+---
229+
230+**Companion posts:**
231+
232+- [The Go audit](/blog/sama-v2-go-project-dive) — where ~5/7 comes from
233+- [The directory-dialect rebuild](/blog/sama-v2-go-project-dive-rebuilt) — the minimal-change variant
234+- [The WordPress audit + rebuild](/blog/sama-v2-wordpress-plugin-audit) — same exercise on a 0/7 starting point
235+- [The §5 metrics post](/blog/sama-v2-metrics-emitter) — why the metrics matter more than the surface syntax
modified src/a31_blog.ts +6 −0
@@ -12,6 +12,12 @@ export interface BlogEntry {
1212 }
1313
1414 export const ALL_POSTS: BlogEntry[] = [
15+ {
16+ slug: "sama-v2-go-project-dive-prefix-scheme",
17+ title: "`dive`, the prefix-scheme variant — what `ls`-readable layer order costs in Go",
18+ description: "The directory-dialect dive rebuild from earlier today lost the visual payoff of the WordPress rebuild's prefix tree. Bas: 'i miss this SAMA v2 style.' Fair. This post shows what dive looks like if it commits to v2.0's prefix scheme literally — every Go file renamed to a_/b1_/b2_/b3_/c1_/c2_/c3_/d_, all 90 sources flat under src/, ls reads top-to-bottom in dependency order, no spec extension required. Then the candid cost: everything ends up in one Go package (losing encapsulation, compilation boundaries, Go's compiler-enforced internal/ semantics), the Law check has to do the work the compiler was doing for free, and the Go community would reject the PR. Two rebuilds (directory dialect + prefix scheme) reach the SAME 7/7 ✓ end state with identical §5 metrics. The spec is stricter than the surface syntax that enforces it — which is itself the design point.",
19+ date: "2026-05-23",
20+ },
1521 {
1622 slug: "sama-v2-go-project-dive-rebuilt",
1723 title: "`dive`, rebuilt under SAMA v2 — a thought experiment",