# `dive`, the prefix-scheme variant — what `ls`-readable layer order costs in Go [Earlier today's `dive` rebuilt sketch](/blog/2026-05/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/2026-05/sama-v2-wordpress-plugin-rebuilt) (`a_meta_tag.php < a_page_context.php < ... < b1_image_selection_policy.php < ... < d_third_party_hooks.php`). 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. 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. ## The full file tree ``` dive/ ├── go.mod # at module root (required by Go) ├── go.sum ├── README.md ├── Makefile ├── Dockerfile ├── Taskfile.yaml │ ├── src/ # all Go sources, flat — accepts non-idiomatic │ │ Go layout for v2.0's lex-sort property │ │ │ │── ─── Layer 0 — Pure ────────────────────────────────────────────────── │ ├── a_bus.go # in-memory event bus type (was internal/bus/bus.go) │ ├── a_bus_helpers.go # (was internal/bus/helpers.go) │ ├── a_event.go # event types (was internal/bus/event/event.go) │ ├── a_event_payload_explore.go # (was internal/bus/event/payload/explore.go) │ ├── a_event_payload_generic.go # (was internal/bus/event/payload/generic.go) │ ├── a_file_node.go # FileNode value type (was dive/filetree/file_node.go) │ ├── a_image.go # Image, ImageMeta types (was dive/image/image.go) │ ├── a_layer.go # Layer type (was dive/image/layer.go) │ ├── a_log.go # log façade (was internal/log/log.go) │ ├── a_node_data.go # NodeData type (was dive/filetree/node_data.go) │ ├── a_order_strategy.go # pure enum (was dive/filetree/order_strategy.go) │ ├── a_path_error.go # (was dive/filetree/path_error.go) │ ├── a_ports.go # NEW: interfaces (Resolver, Loader, EngineClient, ...) │ ├── a_utils_format.go # CleanArgs etc (was internal/utils/format.go) │ ├── a_utils_view.go # (was internal/utils/view.go) │ ├── a_view_info.go # (was dive/filetree/view_info.go) │ │ │ │── ─── Layer 1, sublayer "policy" — pure algorithms ────────────────── │ ├── b1_comparer.go # (was dive/filetree/comparer.go) │ ├── b1_diff.go # (was dive/filetree/diff.go) │ ├── b1_efficiency.go # (was dive/filetree/efficiency.go) │ ├── b1_event_parser.go # (was internal/bus/event/parser/parsers.go) │ ├── b1_file_tree.go # tree algebra (was dive/filetree/file_tree.go) │ ├── b1_image_analysis.go # pure analysis (was dive/image/analysis.go) │ ├── b1_ci_evaluator.go # (was cmd/.../command/ci/evaluator.go) │ ├── b1_ci_rule.go # (was cmd/.../command/ci/rule.go) │ ├── b1_ci_rules.go # (was cmd/.../command/ci/rules.go) │ │ │ │── ─── Layer 1, sublayer "viewmodel" — stateful state managers ──────── │ ├── b2_viewmodel_config.go # (was .../viewmodel/config.go) │ ├── b2_viewmodel_filetree.go # (was .../viewmodel/filetree.go) │ ├── b2_viewmodel_layer_compare.go # (was .../viewmodel/layer_compare.go) │ ├── b2_viewmodel_layer_selection.go # (was .../viewmodel/layer_selection.go) │ ├── b2_viewmodel_layer_set_state.go # (was .../viewmodel/layer_set_state.go) │ ├── b2_format.go # (was .../format/format.go) │ ├── b2_keybindings.go # (was .../key/binding.go + key/config.go merged) │ ├── b2_ui_config.go # (was .../ui/v1/config.go) │ ├── b2_layout_area.go # (was .../layout/area.go) │ ├── b2_layout_location.go # (was .../layout/location.go) │ ├── b2_layout_compound.go # (was .../layout/compound/layer_details_column.go) │ ├── b2_layout_layout.go # (was .../layout/layout.go) │ ├── b2_layout_manager.go # (was .../layout/manager.go) │ │ │ │── ─── Layer 1, sublayer "view" — TUI render (depends on b2 state) ─── │ ├── b3_view_cursor.go # (was .../view/cursor.go) │ ├── b3_view_debug.go # (was .../view/debug.go) │ ├── b3_view_filetree.go # (was .../view/filetree.go) │ ├── b3_view_filter.go # (was .../view/filter.go) │ ├── b3_view_image_details.go # (was .../view/image_details.go) │ ├── b3_view_layer.go # (was .../view/layer.go) │ ├── b3_view_layer_change_listener.go # (was .../view/layer_change_listener.go) │ ├── b3_view_layer_details.go # (was .../view/layer_details.go) │ ├── b3_view_renderer.go # (was .../view/renderer.go) │ ├── b3_view_status.go # (was .../view/status.go) │ ├── b3_view_views.go # (was .../view/views.go) │ │ │ │── ─── Layer 2, sublayer "loader" — filesystem walking ────────────── │ ├── c1_file_info.go # filesystem walking (was dive/filetree/file_info.go) │ │ │ │── ─── Layer 2, sublayer "engine" — Docker/Podman daemon API ──────── │ ├── c2_docker_archive_resolver.go # (was dive/image/docker/archive_resolver.go) │ ├── c2_docker_build.go # (was dive/image/docker/build.go) │ ├── c2_docker_cli.go # (was dive/image/docker/cli.go) │ ├── c2_docker_config.go # (was dive/image/docker/config.go) │ ├── c2_docker_engine_resolver.go # (was dive/image/docker/engine_resolver.go) │ ├── c2_docker_host_unix.go # (was dive/image/docker/docker_host_unix.go) │ ├── c2_docker_host_windows.go # (was dive/image/docker/docker_host_windows.go) │ ├── c2_docker_image_archive.go # (was dive/image/docker/image_archive.go) │ ├── c2_docker_layer.go # (was dive/image/docker/layer.go) │ ├── c2_docker_manifest.go # (was dive/image/docker/manifest.go) │ ├── c2_docker_testing.go # (was dive/image/docker/testing.go) │ ├── c2_image_resolver.go # (was dive/image/resolver.go) │ ├── c2_image_resolver_factory.go # (was dive/get_image_resolver.go) │ ├── c2_podman_build.go # (was dive/image/podman/build.go) │ ├── c2_podman_cli.go # (was dive/image/podman/cli.go) │ ├── c2_podman_resolver.go # (was dive/image/podman/resolver.go) │ ├── c2_podman_resolver_unsupported.go # (was dive/image/podman/resolver_unsupported.go) │ │ │ │── ─── Layer 2, sublayer "config" — YAML parsing ──────────────────── │ ├── c3_options_analysis.go # (was cmd/.../options/analysis.go) │ ├── c3_options_application.go # (was cmd/.../options/application.go) │ ├── c3_options_ci.go # YAML parse .dive-ci.yaml (was .../options/ci.go) │ ├── c3_options_ci_rules.go # (was cmd/.../options/ci_rules.go) │ ├── c3_options_export.go # (was cmd/.../options/export.go) │ ├── c3_options_ui.go # (was cmd/.../options/ui.go) │ ├── c3_options_ui_diff.go # (was cmd/.../options/ui_diff.go) │ ├── c3_options_ui_filetree.go # (was cmd/.../options/ui_filetree.go) │ ├── c3_options_ui_keybindings.go # (was cmd/.../options/ui_keybindings.go) │ ├── c3_options_ui_layers.go # (was cmd/.../options/ui_layers.go) │ │ │ │── ─── Layer 3 — Entry: commands, bootstrap, app controller ───────── │ ├── d_app.go # (was cmd/.../ui/v1/app/app.go) │ ├── d_app_controller.go # (was cmd/.../ui/v1/app/controller.go) │ ├── d_app_job_control_other.go # (was cmd/.../ui/v1/app/job_control_other.go) │ ├── d_app_job_control_unix.go # (was cmd/.../ui/v1/app/job_control_unix.go) │ ├── d_cli.go # (was cmd/dive/cli/cli.go) │ ├── d_cmd_adapter_analyzer.go # (was cmd/.../command/adapter/analyzer.go) │ ├── d_cmd_adapter_evaluator.go # (was cmd/.../command/adapter/evaluator.go) │ ├── d_cmd_adapter_exporter.go # (was cmd/.../command/adapter/exporter.go) │ ├── d_cmd_adapter_resolver.go # (was cmd/.../command/adapter/resolver.go) │ ├── d_cmd_build.go # (was cmd/.../command/build.go) │ ├── d_cmd_export.go # (was cmd/.../command/export/export.go) │ ├── d_cmd_root.go # (was cmd/.../command/root.go) │ ├── d_main.go # (was cmd/dive/main.go) — calls cli.Run() │ ├── d_ui_no_ui.go # (was cmd/.../ui/no_ui.go) │ └── d_ui_v1.go # (was cmd/.../ui/v1.go) │ └── tests/ # one *_test.go per b*_ and c*_ file ├── b1_file_tree_test.go # (was dive/filetree/file_tree_test.go) ├── b1_efficiency_test.go # (was dive/filetree/efficiency_test.go) ├── b1_ci_evaluator_test.go # (was cmd/.../command/ci/evaluator_test.go) ├── b2_viewmodel_filetree_test.go # (was .../viewmodel/filetree_test.go) ├── ... (~40 sibling tests, one per source file in layers 1 and 2) ``` **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. **The profile** (under v2.0 unchanged, no extension needed): ```toml sama_version = "2.0" profile = "dive" [layers.0] prefixes = ["a_"] [layers.1] sublayers = [ { name = "policy", prefix = "b1_" }, { name = "viewmodel", prefix = "b2_" }, { name = "view", prefix = "b3_" }, ] [layers.2] sublayers = [ { name = "loader", prefix = "c1_" }, { name = "engine", prefix = "c2_" }, { name = "config", prefix = "c3_" }, ] [layers.3] prefixes = ["d_"] ``` This passes all seven §4 checks under v2.0 unchanged. No spec extension required. ## What this costs in Go Three concrete prices: **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: - **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. - **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. - **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). **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. **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. ## What the prefix-scheme variant gets right Against those costs, what's bought: - **`ls src/ | sort` reads top-to-bottom in dependency order.** The visual payoff that the [WP rebuild post](/blog/2026-05/sama-v2-wordpress-plugin-rebuilt) leans on works identically here. A reviewer looking at `git diff src/` sees layer changes in their natural order. - **No spec extension needed.** v2.0 as written applies unchanged. The [other rebuild post](/blog/2026-05/sama-v2-go-project-dive-rebuilt) requires a hypothetical v2.1 directory dialect; this one doesn't. - **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. - **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. ## Which variant should the spec actually recommend? Probably the directory dialect for Go, the prefix scheme for most other languages. 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. A v2.1 spec extension would probably say: > *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.* 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. ## What's the same as the other rebuild post Predicted §5 metrics for the prefix-scheme variant are **identical** to the directory-dialect variant: | metric | dive today | dive — directory variant | dive — prefix variant | |---|---|---|---| | §4 checks passing | ~5 / 7 | 7 / 7 (under v2.1 dialect) | 7 / 7 (under v2.0 unchanged) | | graphDepth | ~5 | ~5 | ~5 | | boundaryRatio | ~85% | ~100% | ~100% | | workingSetFit | ~80% | ~80% | ~80% | | violationCounts | ~30 | 0 | 0 | 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. ## Two paths, same destination This is the third `dive` post in a row, and I'll stop after this one. The bracket is now visible: - [The audit](/blog/2026-05/sama-v2-go-project-dive) — `dive` as-is, ~5 of 7 checks pass naturally. - [The directory-dialect rebuild](/blog/2026-05/sama-v2-go-project-dive-rebuilt) — minimal moves, requires a hypothetical v2.1 extension, 7/7 ✓. - This post — the prefix-scheme rebuild, works under v2.0 unchanged, fights Go hard, also 7/7 ✓. 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. --- **Companion posts:** - [The Go audit](/blog/2026-05/sama-v2-go-project-dive) — where ~5/7 comes from - [The directory-dialect rebuild](/blog/2026-05/sama-v2-go-project-dive-rebuilt) — the minimal-change variant - [The WordPress audit + rebuild](/blog/2026-05/sama-v2-wordpress-plugin-audit) — same exercise on a 0/7 starting point - [The §5 metrics post](/blog/2026-05/sama-v2-metrics-emitter) — why the metrics matter more than the surface syntax