e9781414a2d14eb451e1a7123492e024ac0199c3 diff --git a/content/blog/sama-v2-go-project-dive-prefix-scheme.md b/content/blog/sama-v2-go-project-dive-prefix-scheme.md new file mode 100644 index 0000000000000000000000000000000000000000..b17d3853299977615589bf29c4ca04d564798978 --- /dev/null +++ b/content/blog/sama-v2-go-project-dive-prefix-scheme.md @@ -0,0 +1,235 @@ +# `dive`, the prefix-scheme variant — what `ls`-readable layer order costs in Go + +[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`). + +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/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/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/sama-v2-go-project-dive) — `dive` as-is, ~5 of 7 checks pass naturally. +- [The directory-dialect rebuild](/blog/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/sama-v2-go-project-dive) — where ~5/7 comes from +- [The directory-dialect rebuild](/blog/sama-v2-go-project-dive-rebuilt) — the minimal-change variant +- [The WordPress audit + rebuild](/blog/sama-v2-wordpress-plugin-audit) — same exercise on a 0/7 starting point +- [The §5 metrics post](/blog/sama-v2-metrics-emitter) — why the metrics matter more than the surface syntax diff --git a/src/a31_blog.ts b/src/a31_blog.ts index 54935a8cb4a8b270fe4bb817f1d0251b7c91e2bd..9d03b8f8510f83fe7b85c7edf42c4bc1068f5ab5 100644 --- a/src/a31_blog.ts +++ b/src/a31_blog.ts @@ -12,6 +12,12 @@ export interface BlogEntry { } export const ALL_POSTS: BlogEntry[] = [ + { + slug: "sama-v2-go-project-dive-prefix-scheme", + title: "`dive`, the prefix-scheme variant — what `ls`-readable layer order costs in Go", + 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.", + date: "2026-05-23", + }, { slug: "sama-v2-go-project-dive-rebuilt", title: "`dive`, rebuilt under SAMA v2 — a thought experiment",