| name | boundary-hunter-go |
| description | Audit Go packages for boundary violations — leaked internals via exports, coupling through
shared types, import cycles, missing internal/ packages, over-exported APIs, and dependency
direction violations.
Use when: reviewing package structure, shrinking public API surface, enforcing encapsulation,
preparing packages for replacement, or untangling tight coupling between layers.
|
| disable-model-invocation | true |
Boundary Hunter
Audit Go code for package boundary violations — places where implementation details leak through exports, where
packages reach into each other's internals, or where coupling makes replacement impossible. The goal: every package
is a black box, replaceable from its exported API alone.
Go enforces some boundaries at the language level (unexported identifiers, import cycle prohibition, internal/
packages), but many boundary violations are still possible within those constraints.
When to Use
- Reviewing package boundaries before or after a refactor
- Shrinking a package's exported API to what is actually consumed
- Preparing a package to be replaceable (rewritable from its API alone)
- Untangling tight coupling between layers or modules
- Wrapping external dependencies behind internal interfaces
- Enforcing unidirectional dependency flow between layers
Core Principles
-
A package is its exported API. Exported identifiers are promises. Unexported identifiers are implementation
details. Exports should describe what the package does, never how it does it. If a consumer must understand
internals to use the API correctly, the boundary is broken.
-
Minimal exported surface. Export only what is consumed or serves as a deliberate extension point. Every exported
symbol is a coupling point that constrains future changes. Go's convention: start unexported, export when needed.
-
Depend on interfaces, not concretions. Packages should depend on interfaces (defined at the consumer) — not on
concrete types from other packages. If package A imports a concrete struct from package B, A is coupled to B's
implementation. If A defines an interface that B happens to implement, A is coupled only to the contract.
-
Dependency direction must follow architectural intent. In a layered architecture, dependencies flow inward:
infrastructure → application → domain. A domain package importing from infrastructure is a boundary violation.
-
Wrap externals — don't let them leak. Third-party types and APIs should not appear in domain or application
layer exported signatures. Wrap them behind owned types so the external can be replaced without changing consumers.
Infrastructure/adapter packages may use external types in their implementation. The rule is strict for inward
layers, relaxed at the system edge.
-
Use internal/ for enforced encapsulation. Go's internal/ package mechanism provides compiler-enforced
boundary protection. Packages that should not be imported by external consumers belong under internal/.
-
Primitives flow; implementation types stay home. Data that flows between packages should be expressed as
primitive types, standard library types, or shared domain types — not as package-internal structs that force
consumers to import from the implementation. (No tension with smell-hunter's primitive-obsession hunt: "not
implementation structs" and "model domain concepts as owned types" reconcile in shared domain types — a
UserID named type flowing between packages satisfies both.)
What to Hunt
1. Over-Exported API Surface
Exported symbols that serve no external consumer — internal helpers, intermediate types, or implementation utilities
that happen to start with an uppercase letter.
Signals:
- Exported function/type with zero import sites outside its own package
- Exported type that includes implementation-specific fields (internal caches, state machines)
- Exported helpers/utilities alongside domain API
- Exported constructor for a type that should be package-private
Action: Unexport the symbol. If consumed externally, evaluate whether the consumer should own the concept.
2. Missing internal/ Packages
Implementation packages that are importable by external consumers but shouldn't be.
Signals:
- Utility packages under
pkg/ that are only used by sibling packages in the same module
- Shared helper packages that are not part of the public API
- Packages containing implementation details that external consumers could accidentally depend on
- Infrastructure adapters that should not be directly imported by domain packages
Action: Move to internal/ to enforce the boundary at the compiler level.
3. Coupling Through Shared Types
Two packages that share a type where neither owns it, or where one package's internal type appears in another
package's function signatures.
Signals:
- Package A imports a type defined in package B that is not part of B's intended public API
- A "shared types" package that grows unboundedly, coupling all importers
- Function signature contains a parameter typed as another package's internal struct
- Domain types defined in infrastructure packages
Action: Move shared types to a dedicated domain/contracts package owned by neither. Or define the interface in the
consumer and have the producer conform to it.
4. Deep Import Paths
Consumers importing sub-packages that should be internal to a parent package.
Signals:
- Imports like
github.com/org/repo/pkg/auth/internal/tokens from outside auth/
- Imports reaching into implementation sub-packages (
service/impl/, handler/private/)
- Multiple import paths for the same concept (re-exported inconsistently)
Action: Use internal/ to enforce boundaries. Expose needed symbols through the parent package's API.
5. Dependency Direction Violations
A lower-level package importing from a higher-level package, breaking the intended layering.
Signals:
- Domain package importing from infrastructure or transport layer
- Shared utility importing from a feature package
- A "core" package that depends on a "feature" package
- Model/entity package importing from handler/controller package
Action: Invert the dependency. Define an interface in the lower layer; implement it in the higher layer. Wire via
dependency injection at the composition root.
6. External Type Leaks
Third-party library types appearing in exported signatures of domain or application packages.
Signals:
- Exported function parameter or return type from a third-party module in a domain package
- Package re-exports a third-party type as part of its own API
- Switching the underlying library would require changing consumer code
- Framework-specific types (e.g.,
gin.Context, echo.Context) in domain or application packages
Acceptable: Infrastructure/adapter packages using external types in their exported API — they are the wrapping
layer. Flag only when these types leak inward into domain/application consumers.
Action: Define an owned interface or type that wraps the external. The wrapping package is the only place that
imports from the external. Consumers depend on the owned type.
7. Package Naming and Organization Issues
Packages named by what they contain rather than what they provide. Ownership: package naming and organization
is owned here as a boundary-legibility question — solid-hunter analyzes the same packages through a
responsibility/change lens (multiple actors, reasons to change), not naming.
Signals:
- Packages named
util, utils, helpers, common, misc, base, shared
- Package name that doesn't match the directory name
- Package
models that contains types for unrelated domains
- Deeply nested package paths for simple concepts
- Multiple files in a package spanning unrelated concerns
Action: Name packages after their domain concept or capability. Split mixed-concern packages. Flatten unnecessary
nesting.
Audit Workflow
Phase 1: Map Package Boundaries
-
Resolve audit surface. The prompt may specify the scope as:
- Diff: files changed relative to the base branch — committed, staged, unstaged, and untracked
- Path: specific files, folders, or packages
- Codebase: the entire project (the default when unspecified)
Party mode: when the orchestrator supplies a scope snapshot (a resolved file list), use it verbatim and do
not re-resolve. The resolution below applies to standalone runs only.
For diff mode, resolve fail-closed:
BASE=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/@@')
if [ -z "$BASE" ]; then
for b in origin/main origin/master main master; do
git rev-parse -q --verify "$b" >/dev/null && BASE=$b && break
done
fi
SCOPE=$( { git diff --name-only --diff-filter=d "$BASE"...HEAD;
git diff --name-only --diff-filter=d HEAD;
git ls-files --others --exclude-standard; } | sort -u )
DELETED=$( { git diff --name-only --diff-filter=D "$BASE"...HEAD;
git diff --name-only --diff-filter=D HEAD; } | sort -u )
If $SCOPE is empty, run no analysis: write the report with "Audit completed: 0 findings — empty diff scope",
listing $DELETED under "Deleted in diff" if non-empty, and stop. If the resolved surface exceeds what can be
read within the context budget, report the file count and ask to narrow or chunk.
Two surfaces. This hunter analyzes an import graph, so the analysis (import mapping, fan-in/fan-out,
dead-export consumer counting) legitimately runs project-wide — a package's consumers live outside any diff
scope. Findings are still reported only against the target scope: every finding anchors (file:line) to
in-scope packages.
-
Identify packages. List all Go packages and their import paths. Note internal/ packages.
-
Catalogue exports. For each package, list exported types, functions, and variables. Classify each as:
type, function, constant, variable.
-
List external dependencies. For each package, list imports from outside the module. Note which external types
appear in exported signatures.
Phase 2: Analyze Dependency Graph
- Build import map. For each package, list which other internal packages it imports from.
rg '^import' --type go -A 20 --glob '!**/vendor/**' --glob '!**/*_test.go'
go list -f '{{.ImportPath}} {{join .Imports " "}}' ./...
- Check direction. If the project has an intended layering (domain → application → infrastructure), verify all
import arrows point in the correct direction.
- Check for
internal/ usage. Are implementation packages properly under internal/?
- Measure fan-in / fan-out. Packages with high fan-in (many importers) are stability anchors — changes are costly.
Packages with high fan-out (many imports) are fragile.
Phase 3: Audit Export Surface
For each package:
-
Dead exports. Is every exported symbol consumed by at least one external package?
EXCLUDE='--glob !**/vendor/** --glob !**/testdata/** --glob !**/*_test.go --glob !**/*.pb.go --glob !**/*_gen.go --glob !**/*_generated.go'
rg 'func\s+[A-Z]|type\s+[A-Z]|var\s+[A-Z]|const\s+[A-Z]' --type go $EXCLUDE
rg 'SymbolName' --type go $EXCLUDE
Verify each candidate finding manually — a grep miss is not proof of zero usage.
Library caveat. Zero in-module references does not make an exported symbol dead when the module is
imported by external consumers. Check whether the module is a published library (go.mod module path, known
importers) and label such candidates "unused internally", not dead. golang.org/x/tools/cmd/deadcode
gives call-graph (RTA) evidence of unreachable functions in modules with executable main packages —
useful supporting evidence, but not a symbol-level census: it says nothing about exported types, constants,
or variables, and cannot establish deadness of a library's public API.
-
Leaked internals. Do any exports expose implementation details?
-
External type leaks. Do any exports use third-party types in their signatures?
Phase 4: Audit Consumer Access Patterns
For each package's consumers:
- Deep imports. Are consumers importing sub-packages that should be internal?
- Knowledge coupling. Do consumers make decisions based on implementation details of the imported package?
Phase 5: Evaluate Replaceability
For each package, answer:
- Could this package be rewritten from its exported API alone? If a new developer needed to rewrite the package,
would the exported types + function signatures be sufficient?
- What would break if the implementation changed completely? If the answer is "only the package's tests", the
boundary is clean.
- Are there implicit contracts not captured in the API? Ordering guarantees, side effects, goroutine safety,
context cancellation behavior — anything consumers depend on that isn't in the type signature.
Output Format
Save as YYYY-MM-DD-boundary-hunter-audit-{model-name}.md — {model-name} is the executing model's short name
(e.g. fable-5) — in the project's docs folder (or project root if no docs folder exists). If the caller specifies
an output path or return mode (e.g. the party-hunter orchestrator), it overrides this default.
Severity levels, used for per-finding labels and the Recommendations grouping:
- Critical — exploitable now, causes data loss, or breaks behavior on production paths.
- High — a defect with likely user-visible, security, or reliability impact if left unaddressed.
- Medium — correctness or maintainability risk without imminent impact.
- Low — hygiene; no behavioral risk.
# Boundary Hunter Audit — {date}
## Scope
- Surface: {diff / path / codebase}
- Files: {count or list}
- Exclusions: {list}
- {Deleted in diff: {list} — only for diff scope with deletions}
- Audit completed: {N} findings
## Package Map
| Package | Exported Symbols | External Deps | Fan-In | Fan-Out |
| ------- | ---------------- | ------------- | ------ | ------- |
| domain/order | 5 types, 2 fns | 0 | 8 | 1 |
| infra/postgres | 3 fns | 1 (pgx) | 2 | 4 |
## Dependency Graph Issues
### Direction Violations
- domain/X imports from infra/Y ({symbol}, {file:line})
## Export Surface Issues
### Over-Exported API
| # | Package | Export | Type | External Consumers |
| - | ------- | ------ | ---- | ------------------ |
| 1 | pkg/auth | `helperFn` | function | 0 |
### Missing internal/ Packages
| # | Package | Reason | Action |
| - | ------- | ------ | ------ |
| 1 | pkg/crypto/impl | Only used by pkg/crypto | Move to internal/ |
### External Type Leaks
| # | Package | Export / Signature | External Type | Action |
| - | ------- | ------------------ | ------------- | ------ |
| 1 | domain/user | `Save(ctx context.Context, tx pgx.Tx)` | `pgx.Tx` | Wrap behind owned interface |
## Consumer Access Violations
### Deep Imports
| # | Consumer | Imported Path | Should Use |
| - | -------- | ------------- | ---------- |
| 1 | file.go:line | `auth/internal/tokens` | `auth` |
## Package Naming Issues
| # | Package | Issue | Action |
| - | ------- | ----- | ------ |
| 1 | `utils` | Generic name, mixed concerns | Split by capability |
## Replaceability Assessment
### {Package Name}
- Replaceable from API? {yes/no — why}
- Implicit contracts: {goroutine safety, ordering, side effects}
- Coupling risk: {low/med/high}
## Recommendations (Priority Order)
1. **High**: {direction violations, external leaks in domain layer}
2. **Medium**: {dead exports, missing internal/, over-exported API}
3. **Low**: {replaceability improvements, package naming, deep imports}
Operating Constraints
- No code edits. This skill produces an audit report only. Implementation is a separate step.
- No empty finding sections. Include only categories with findings. Omit a heading, table, or list entirely when it would contain zero items — do not include empty tables, placeholder subsections, or negative statements like "no dead exports", "none found", or "no issues". Execution status is exempt: the "Audit completed: N findings" line in the Scope section is always present, even at zero findings.
- Scope: package boundaries only. Encapsulation, coupling, dependency direction, API surface. If a finding
doesn't answer "is this boundary clean?", it belongs to another hunter — do not flag it here. Named boundary:
package naming and organization is owned here (§7); solid-hunter keeps SRP as responsibility/change analysis.
- Evidence required. Every finding must cite
file/path.go:line with the exact code or import statement.
- Architecture-first. Understand the project's intended layering before flagging violations. Ask if unclear.
- Pragmatism over purism. Not every coupling is worth breaking. Small utilities shared between two closely related
packages may be fine. Flag, but don't insist on architectural astronautics.
- Measure, don't guess. Use
rg, go list, and import analysis to count actual consumers.
- Respect Go conventions. Go packages are designed to be flat and focused. Don't impose Java-style deep package
hierarchies. Go's
internal/ mechanism is the primary boundary enforcement tool.