sama-v2-go-project-dive-prefix-scheme.md
raw
· source
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