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/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 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 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 auditdive as-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: