diff --git a/docs/roadmap/remediation.md b/docs/roadmap/remediation.md index c57c5e4..35a0a8e 100644 --- a/docs/roadmap/remediation.md +++ b/docs/roadmap/remediation.md @@ -358,6 +358,8 @@ fetch endpoint reports absence. Focused publication/wrapper tests pass. ### R10 — Medium: The boundary checker bans valid command-internal composition +Status: Resolved. + Category: Inadvertently strengthened architectural prohibition. **Evidence:** [checkImportDirections](../../internal/apicheck/main.go), around @@ -381,6 +383,12 @@ turning current package inventory into a fixed future architecture. internal imports of command code and internal-helper imports of public facades remain rejected. +**Resolution:** The import checker identifies each executable’s internal helper +subtree and permits collaboration inside it. Library-to-command imports, +helper-to-controller imports, and internal-to-facade imports remain rejected. +Positive and negative dependency fixtures and the actual compiled repository +boundary checks pass. + ## Remediation boundaries Address these findings before treating the initial implementation as complete diff --git a/internal/apicheck/main.go b/internal/apicheck/main.go index eac487c..083bb98 100644 --- a/internal/apicheck/main.go +++ b/internal/apicheck/main.go @@ -112,7 +112,10 @@ func checkImportDirections(packages []listedPackage) error { continue } for _, imported := range pkg.Imports { - if imported == modulePath || imported == modulePath+"/buildinfo" || strings.HasPrefix(imported, modulePath+"/cmd/") { + commandDependency := strings.HasPrefix(imported, modulePath+"/cmd/") + owner := commandInternalRoot(pkg.ImportPath) + ownHelper := owner != "" && strings.HasPrefix(imported+"/", owner) + if imported == modulePath || imported == modulePath+"/buildinfo" || commandDependency && !ownHelper { violations = append(violations, fmt.Sprintf("internal package %s imports public or command package %s", pkg.ImportPath, imported)) } } @@ -150,8 +153,21 @@ func checkImportDirections(packages []listedPackage) error { } func isInternalPackage(path string) bool { - return strings.HasPrefix(path, modulePath+"/internal/") || - strings.HasPrefix(path, modulePath+"/") && strings.Contains(strings.TrimPrefix(path, modulePath), "/internal/") + return strings.HasPrefix(path, modulePath+"/") && + (strings.Contains(strings.TrimPrefix(path, modulePath), "/internal/") || strings.HasSuffix(path, "/internal")) +} + +// commandInternalRoot identifies the helper subtree owned by one executable. +func commandInternalRoot(path string) string { + if !strings.HasPrefix(path, modulePath+"/cmd/") { + return "" + } + relative := strings.TrimPrefix(path, modulePath+"/cmd/") + owner, _, ok := strings.Cut(relative+"/", "/internal/") + if !ok { + return "" + } + return modulePath + "/cmd/" + owner + "/internal/" } func forbiddenRuntimeDependency(path string) bool { diff --git a/internal/apicheck/main_test.go b/internal/apicheck/main_test.go index 8d54be5..9ba47ed 100644 --- a/internal/apicheck/main_test.go +++ b/internal/apicheck/main_test.go @@ -182,3 +182,27 @@ func typecheckWithImporter(t *testing.T, fset *token.FileSet, path, source strin } return pkg } + +func TestCommandInternalCompositionPreservesLibraryBoundaries(t *testing.T) { + for _, test := range []struct { + name, from, to string + reject bool + }{ + {"command helpers", "/cmd/releasekit/internal/cli/parser", "/cmd/releasekit/internal/cli/settings", false}, + {"other executable helpers", "/cmd/future/internal/parser", "/cmd/future/internal/settings", false}, + {"helper root", "/cmd/releasekit/internal", "/cmd/releasekit/internal/cli", false}, + {"controller uses facade", "/cmd/releasekit", "", false}, + {"library imports controller", "/internal/build", "/cmd/releasekit", true}, + {"library imports helper", "/internal/build", "/cmd/releasekit/internal/cli", true}, + {"helper imports facade", "/cmd/releasekit/internal/cli", "", true}, + {"helper imports runtime facade", "/cmd/releasekit/internal/cli", "/buildinfo", true}, + {"helper imports controller", "/cmd/releasekit/internal/cli", "/cmd/releasekit", true}, + } { + t.Run(test.name, func(t *testing.T) { + err := checkImportDirections([]listedPackage{{ImportPath: modulePath + test.from, Imports: []string{modulePath + test.to}}}) + if (err != nil) != test.reject { + t.Fatalf("dependency %s -> %s: %v", test.from, test.to, err) + } + }) + } +}