chore: bootstrap vev — hexagonal skeleton, deps, import guard
- commit
- 16d10fa43c0cac8d074304ae7c211e54cf101f3b
- author
- brice · · 2026-07-02 09:03 UTC
- parent
- none (root commit)
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
15 files · +158 -0
A .gitignore +3 -0
| @@ -0,0 +1,3 @@ | |||
| 1 | + | /vev | |
| 2 | + | *.test | |
| 3 | + | .superpowers/ | |
| @@ -0,0 +1,3 @@ | |||||
| 1 | + | /vev | |||
| 2 | + | *.test | |||
| 3 | + | .superpowers/ | |||
A .mockery.yml +30 -0
| @@ -0,0 +1,30 @@ | |||
| 1 | + | all: false | |
| 2 | + | dir: '{{.InterfaceDir}}' | |
| 3 | + | filename: mocks_test.go | |
| 4 | + | force-file-write: true | |
| 5 | + | formatter: goimports | |
| 6 | + | formatter-options: | |
| 7 | + | goimports: | |
| 8 | + | all-errors: false | |
| 9 | + | comments: true | |
| 10 | + | format-only: true | |
| 11 | + | fragment: false | |
| 12 | + | local-prefix: "" | |
| 13 | + | tab-indent: true | |
| 14 | + | tab-width: 8 | |
| 15 | + | generate: true | |
| 16 | + | include-auto-generated: false | |
| 17 | + | log-level: info | |
| 18 | + | structname: '{{.Mock}}{{.InterfaceName}}' | |
| 19 | + | pkgname: '{{.SrcPackageName}}' | |
| 20 | + | recursive: false | |
| 21 | + | require-template-schema-exists: true | |
| 22 | + | template: testify | |
| 23 | + | template-schema: '{{.Template}}.schema.json' | |
| 24 | + | packages: | |
| 25 | + | github.com/bnema/vev/internal/ports: | |
| 26 | + | config: | |
| 27 | + | all: true | |
| 28 | + | dir: internal/ports/mocks | |
| 29 | + | filename: mocks.go | |
| 30 | + | pkgname: portsmocks | |
| @@ -0,0 +1,30 @@ | |||||
| 1 | + | all: false | |||
| 2 | + | dir: '{{.InterfaceDir}}' | |||
| 3 | + | filename: mocks_test.go | |||
| 4 | + | force-file-write: true | |||
| 5 | + | formatter: goimports | |||
| 6 | + | formatter-options: | |||
| 7 | + | goimports: | |||
| 8 | + | all-errors: false | |||
| 9 | + | comments: true | |||
| 10 | + | format-only: true | |||
| 11 | + | fragment: false | |||
| 12 | + | local-prefix: "" | |||
| 13 | + | tab-indent: true | |||
| 14 | + | tab-width: 8 | |||
| 15 | + | generate: true | |||
| 16 | + | include-auto-generated: false | |||
| 17 | + | log-level: info | |||
| 18 | + | structname: '{{.Mock}}{{.InterfaceName}}' | |||
| 19 | + | pkgname: '{{.SrcPackageName}}' | |||
| 20 | + | recursive: false | |||
| 21 | + | require-template-schema-exists: true | |||
| 22 | + | template: testify | |||
| 23 | + | template-schema: '{{.Template}}.schema.json' | |||
| 24 | + | packages: | |||
| 25 | + | github.com/bnema/vev/internal/ports: | |||
| 26 | + | config: | |||
| 27 | + | all: true | |||
| 28 | + | dir: internal/ports/mocks | |||
| 29 | + | filename: mocks.go | |||
| 30 | + | pkgname: portsmocks | |||
A boundary_test.go +74 -0
| @@ -0,0 +1,74 @@ | |||
| 1 | + | package main_test | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "go/parser" | |
| 5 | + | "go/token" | |
| 6 | + | "io/fs" | |
| 7 | + | "path/filepath" | |
| 8 | + | "strings" | |
| 9 | + | "testing" | |
| 10 | + | ) | |
| 11 | + | ||
| 12 | + | // boundaryRule expresses a single import-boundary constraint: no .go file | |
| 13 | + | // under root may import a package whose path has disallowedPrefix. | |
| 14 | + | type boundaryRule struct { | |
| 15 | + | name string | |
| 16 | + | root string | |
| 17 | + | disallowedPrefix string | |
| 18 | + | } | |
| 19 | + | ||
| 20 | + | func TestImportBoundaries(t *testing.T) { | |
| 21 | + | rules := []boundaryRule{ | |
| 22 | + | { | |
| 23 | + | name: "pkg must not import internal", | |
| 24 | + | root: "pkg", | |
| 25 | + | disallowedPrefix: "github.com/bnema/vev/internal", | |
| 26 | + | }, | |
| 27 | + | } | |
| 28 | + | ||
| 29 | + | for _, rule := range rules { | |
| 30 | + | t.Run(rule.name, func(t *testing.T) { | |
| 31 | + | violations := findImportViolations(t, rule.root, rule.disallowedPrefix) | |
| 32 | + | if len(violations) > 0 { | |
| 33 | + | t.Errorf("found disallowed imports of %q under %q:\n%s", rule.disallowedPrefix, rule.root, strings.Join(violations, "\n")) | |
| 34 | + | } | |
| 35 | + | }) | |
| 36 | + | } | |
| 37 | + | } | |
| 38 | + | ||
| 39 | + | // findImportViolations walks root, parses each .go file's imports, and | |
| 40 | + | // returns a description for every import whose path starts with | |
| 41 | + | // disallowedPrefix. | |
| 42 | + | func findImportViolations(t *testing.T, root, disallowedPrefix string) []string { | |
| 43 | + | t.Helper() | |
| 44 | + | ||
| 45 | + | var violations []string | |
| 46 | + | fset := token.NewFileSet() | |
| 47 | + | ||
| 48 | + | err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error { | |
| 49 | + | if err != nil { | |
| 50 | + | return err | |
| 51 | + | } | |
| 52 | + | if d.IsDir() || !strings.HasSuffix(path, ".go") { | |
| 53 | + | return nil | |
| 54 | + | } | |
| 55 | + | ||
| 56 | + | f, err := parser.ParseFile(fset, path, nil, parser.ImportsOnly) | |
| 57 | + | if err != nil { | |
| 58 | + | return err | |
| 59 | + | } | |
| 60 | + | ||
| 61 | + | for _, imp := range f.Imports { | |
| 62 | + | importPath := strings.Trim(imp.Path.Value, `"`) | |
| 63 | + | if strings.HasPrefix(importPath, disallowedPrefix) { | |
| 64 | + | violations = append(violations, path+": imports "+importPath) | |
| 65 | + | } | |
| 66 | + | } | |
| 67 | + | return nil | |
| 68 | + | }) | |
| 69 | + | if err != nil { | |
| 70 | + | t.Fatalf("walking %s: %v", root, err) | |
| 71 | + | } | |
| 72 | + | ||
| 73 | + | return violations | |
| 74 | + | } | |
| @@ -0,0 +1,74 @@ | |||||
| 1 | + | package main_test | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "go/parser" | |||
| 5 | + | "go/token" | |||
| 6 | + | "io/fs" | |||
| 7 | + | "path/filepath" | |||
| 8 | + | "strings" | |||
| 9 | + | "testing" | |||
| 10 | + | ) | |||
| 11 | + | ||||
| 12 | + | // boundaryRule expresses a single import-boundary constraint: no .go file | |||
| 13 | + | // under root may import a package whose path has disallowedPrefix. | |||
| 14 | + | type boundaryRule struct { | |||
| 15 | + | name string | |||
| 16 | + | root string | |||
| 17 | + | disallowedPrefix string | |||
| 18 | + | } | |||
| 19 | + | ||||
| 20 | + | func TestImportBoundaries(t *testing.T) { | |||
| 21 | + | rules := []boundaryRule{ | |||
| 22 | + | { | |||
| 23 | + | name: "pkg must not import internal", | |||
| 24 | + | root: "pkg", | |||
| 25 | + | disallowedPrefix: "github.com/bnema/vev/internal", | |||
| 26 | + | }, | |||
| 27 | + | } | |||
| 28 | + | ||||
| 29 | + | for _, rule := range rules { | |||
| 30 | + | t.Run(rule.name, func(t *testing.T) { | |||
| 31 | + | violations := findImportViolations(t, rule.root, rule.disallowedPrefix) | |||
| 32 | + | if len(violations) > 0 { | |||
| 33 | + | t.Errorf("found disallowed imports of %q under %q:\n%s", rule.disallowedPrefix, rule.root, strings.Join(violations, "\n")) | |||
| 34 | + | } | |||
| 35 | + | }) | |||
| 36 | + | } | |||
| 37 | + | } | |||
| 38 | + | ||||
| 39 | + | // findImportViolations walks root, parses each .go file's imports, and | |||
| 40 | + | // returns a description for every import whose path starts with | |||
| 41 | + | // disallowedPrefix. | |||
| 42 | + | func findImportViolations(t *testing.T, root, disallowedPrefix string) []string { | |||
| 43 | + | t.Helper() | |||
| 44 | + | ||||
| 45 | + | var violations []string | |||
| 46 | + | fset := token.NewFileSet() | |||
| 47 | + | ||||
| 48 | + | err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error { | |||
| 49 | + | if err != nil { | |||
| 50 | + | return err | |||
| 51 | + | } | |||
| 52 | + | if d.IsDir() || !strings.HasSuffix(path, ".go") { | |||
| 53 | + | return nil | |||
| 54 | + | } | |||
| 55 | + | ||||
| 56 | + | f, err := parser.ParseFile(fset, path, nil, parser.ImportsOnly) | |||
| 57 | + | if err != nil { | |||
| 58 | + | return err | |||
| 59 | + | } | |||
| 60 | + | ||||
| 61 | + | for _, imp := range f.Imports { | |||
| 62 | + | importPath := strings.Trim(imp.Path.Value, `"`) | |||
| 63 | + | if strings.HasPrefix(importPath, disallowedPrefix) { | |||
| 64 | + | violations = append(violations, path+": imports "+importPath) | |||
| 65 | + | } | |||
| 66 | + | } | |||
| 67 | + | return nil | |||
| 68 | + | }) | |||
| 69 | + | if err != nil { | |||
| 70 | + | t.Fatalf("walking %s: %v", root, err) | |||
| 71 | + | } | |||
| 72 | + | ||||
| 73 | + | return violations | |||
| 74 | + | } | |||
A go.mod +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | module github.com/bnema/vev | |
| 2 | + | ||
| 3 | + | go 1.26 | |
| 4 | + | ||
| 5 | + | require ( | |
| 6 | + | golang.org/x/sys v0.46.0 // indirect | |
| 7 | + | golang.org/x/term v0.44.0 // indirect | |
| 8 | + | ) | |
| @@ -0,0 +1,8 @@ | |||||
| 1 | + | module github.com/bnema/vev | |||
| 2 | + | ||||
| 3 | + | go 1.26 | |||
| 4 | + | ||||
| 5 | + | require ( | |||
| 6 | + | golang.org/x/sys v0.46.0 // indirect | |||
| 7 | + | golang.org/x/term v0.44.0 // indirect | |||
| 8 | + | ) | |||
A go.sum +4 -0 generated file: collapsed
A internal/adapters/ipc/doc.go +2 -0
| @@ -0,0 +1,2 @@ | |||
| 1 | + | // Package ipc adapts vev's ports to inter-process communication implementations. | |
| 2 | + | package ipc | |
| @@ -0,0 +1,2 @@ | |||||
| 1 | + | // Package ipc adapts vev's ports to inter-process communication implementations. | |||
| 2 | + | package ipc | |||
A internal/adapters/pty/doc.go +2 -0
| @@ -0,0 +1,2 @@ | |||
| 1 | + | // Package pty adapts vev's ports to pseudo-terminal implementations. | |
| 2 | + | package pty | |
| @@ -0,0 +1,2 @@ | |||||
| 1 | + | // Package pty adapts vev's ports to pseudo-terminal implementations. | |||
| 2 | + | package pty | |||
A internal/adapters/term/doc.go +2 -0
| @@ -0,0 +1,2 @@ | |||
| 1 | + | // Package term adapts vev's ports to terminal I/O implementations. | |
| 2 | + | package term | |
| @@ -0,0 +1,2 @@ | |||||
| 1 | + | // Package term adapts vev's ports to terminal I/O implementations. | |||
| 2 | + | package term | |||
A internal/app/run.go +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | // Package app wires vev's runtime together via dependency injection. | |
| 2 | + | package app | |
| 3 | + | ||
| 4 | + | // Run is the entry point invoked by main. It parses no flags yet and | |
| 5 | + | // performs no work; later milestones will wire the application here. | |
| 6 | + | func Run(args []string) error { | |
| 7 | + | return nil | |
| 8 | + | } | |
| @@ -0,0 +1,8 @@ | |||||
| 1 | + | // Package app wires vev's runtime together via dependency injection. | |||
| 2 | + | package app | |||
| 3 | + | ||||
| 4 | + | // Run is the entry point invoked by main. It parses no flags yet and | |||
| 5 | + | // performs no work; later milestones will wire the application here. | |||
| 6 | + | func Run(args []string) error { | |||
| 7 | + | return nil | |||
| 8 | + | } | |||
A internal/domain/doc.go +2 -0
| @@ -0,0 +1,2 @@ | |||
| 1 | + | // Package domain contains vev's pure domain types. | |
| 2 | + | package domain | |
| @@ -0,0 +1,2 @@ | |||||
| 1 | + | // Package domain contains vev's pure domain types. | |||
| 2 | + | package domain | |||
A internal/ports/doc.go +2 -0
| @@ -0,0 +1,2 @@ | |||
| 1 | + | // Package ports defines the interfaces that decouple vev's core from its adapters. | |
| 2 | + | package ports | |
| @@ -0,0 +1,2 @@ | |||||
| 1 | + | // Package ports defines the interfaces that decouple vev's core from its adapters. | |||
| 2 | + | package ports | |||
A internal/usecase/doc.go +2 -0
| @@ -0,0 +1,2 @@ | |||
| 1 | + | // Package usecase implements vev's application use cases. | |
| 2 | + | package usecase | |
| @@ -0,0 +1,2 @@ | |||||
| 1 | + | // Package usecase implements vev's application use cases. | |||
| 2 | + | package usecase | |||
A main.go +15 -0
| @@ -0,0 +1,15 @@ | |||
| 1 | + | package main | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "fmt" | |
| 5 | + | "os" | |
| 6 | + | ||
| 7 | + | "github.com/bnema/vev/internal/app" | |
| 8 | + | ) | |
| 9 | + | ||
| 10 | + | func main() { | |
| 11 | + | if err := app.Run(os.Args[1:]); err != nil { | |
| 12 | + | fmt.Fprintln(os.Stderr, "vev:", err) | |
| 13 | + | os.Exit(1) | |
| 14 | + | } | |
| 15 | + | } | |
| @@ -0,0 +1,15 @@ | |||||
| 1 | + | package main | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "fmt" | |||
| 5 | + | "os" | |||
| 6 | + | ||||
| 7 | + | "github.com/bnema/vev/internal/app" | |||
| 8 | + | ) | |||
| 9 | + | ||||
| 10 | + | func main() { | |||
| 11 | + | if err := app.Run(os.Args[1:]); err != nil { | |||
| 12 | + | fmt.Fprintln(os.Stderr, "vev:", err) | |||
| 13 | + | os.Exit(1) | |||
| 14 | + | } | |||
| 15 | + | } | |||
A pkg/renderer/doc.go +2 -0
| @@ -0,0 +1,2 @@ | |||
| 1 | + | // Package renderer draws terminal state to the screen. | |
| 2 | + | package renderer | |
| @@ -0,0 +1,2 @@ | |||||
| 1 | + | // Package renderer draws terminal state to the screen. | |||
| 2 | + | package renderer | |||
A pkg/vt/doc.go +2 -0
| @@ -0,0 +1,2 @@ | |||
| 1 | + | // Package vt implements a terminal emulator core. | |
| 2 | + | package vt | |
| @@ -0,0 +1,2 @@ | |||||
| 1 | + | // Package vt implements a terminal emulator core. | |||
| 2 | + | package vt | |||