dive, the prefix-scheme variant — what ls-readable layer order costs in Go
Earlier today's dive rebuilt sketch 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 (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):
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/filetreepackage can exportFileTreewhile keepingfile_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
filetreedoesn't forceimage/dockerto 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/ | sortreads top-to-bottom in dependency order. The visual payoff that the WP rebuild post leans on works identically here. A reviewer looking atgit diff src/sees layer changes in their natural order.- No spec extension needed. v2.0 as written applies unchanged. The other rebuild post 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
layoutmodes:"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 —
diveas-is, ~5 of 7 checks pass naturally. - The directory-dialect rebuild — 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 — where ~5/7 comes from
- The directory-dialect rebuild — the minimal-change variant
- The WordPress audit + rebuild — same exercise on a 0/7 starting point
- The §5 metrics post — why the metrics matter more than the surface syntax