Thanks for your interest in contributing!
git clone https://github.com/matzehuels/stacktower.git
cd stacktower
make install-tools # Install golangci-lint, goimports, govulncheck
make check # Run all CI checks locally- Fork the repository
- Create a feature branch (
git checkout -b feat/amazing-feature) - Make your changes
- Run checks:
make check - Commit with Conventional Commits format:
feat: add new featurefix: resolve bugdocs: update readmerefactor: restructure codetest: add testsci: update workflows
- Push and open a Pull Request
- Run
make fmtbefore committing - Run
make lintto check for issues - Keep changes focused and minimal
make test # Unit tests
make e2e # End-to-end tests
make cover # Tests with coverageStacktower follows a clean layered architecture. See the pkg.go.dev documentation for detailed API docs.
internal/cli/ # Command-line interface
pkg/dag/ # Core DAG data structure
├── transform/ # Graph normalization (transitive reduction, subdivision)
└── perm/ # PQ-tree and permutation algorithms
pkg/deps/ # Dependency resolution from registries
├── python/ # Python: PyPI + poetry.lock + requirements.txt
├── rust/ # Rust: crates.io + Cargo.toml
├── javascript/ # JavaScript: npm + package.json
├── ruby/ # Ruby: RubyGems + Gemfile
├── php/ # PHP: Packagist + composer.json
├── java/ # Java: Maven Central + pom.xml
├── golang/ # Go: Go Module Proxy + go.mod
└── metadata/ # GitHub/GitLab enrichment providers
pkg/integrations/ # Registry API clients (npm, pypi, crates, etc.)
pkg/render/tower/ # Tower visualization
├── ordering/ # Barycentric and optimal ordering algorithms
├── layout/ # Block position computation
├── sink/ # Output formats (SVG, JSON, PDF, PNG)
└── styles/ # Visual styles (handdrawn, simple)
pkg/io/ # JSON import/export
- Create an integration client in
pkg/integrations/<registry>/client.go:
type Client struct {
*integrations.Client
baseURL string
}
func NewClient(cacheTTL time.Duration) (*Client, error) {
cache, err := integrations.NewCache(cacheTTL)
if err != nil {
return nil, err
}
return &Client{
Client: integrations.NewClient(cache, nil),
baseURL: "https://registry.example.com",
}, nil
}
func (c *Client) FetchPackage(ctx context.Context, name string, refresh bool) (*PackageInfo, error) {
// Implement caching and fetching
}- Create a language definition in
pkg/deps/<lang>/<lang>.go:
var Language = &deps.Language{
Name: "mylang",
DefaultRegistry: "myregistry",
RegistryAliases: map[string]string{"alias": "myregistry"},
ManifestTypes: []string{"my.lock"},
ManifestAliases: map[string]string{"my.lock": "mylock"},
NewResolver: newResolver,
NewManifest: newManifest,
ManifestParsers: manifestParsers,
}- Register in CLI in
internal/cli/parse.go
See pkg/deps for detailed documentation.
Implement the ManifestParser interface:
type MyLockParser struct{}
func (p *MyLockParser) Type() string { return "my.lock" }
func (p *MyLockParser) IncludesTransitive() bool { return true }
func (p *MyLockParser) Supports(name string) bool { return name == "my.lock" }
func (p *MyLockParser) Parse(path string, opts deps.Options) (*deps.ManifestResult, error) {
g := dag.New(nil)
// ... populate nodes and edges
return &deps.ManifestResult{Graph: g, Type: p.Type(), IncludesTransitive: true}, nil
}Output formats are "sinks" in pkg/render/tower/sink/. Each sink takes a layout.Layout and renders it to bytes.
- Create a sink file in
pkg/render/tower/sink/<format>.go:
func RenderMyFormat(l layout.Layout, opts ...MyFormatOption) ([]byte, error) {
// Access layout data:
// - l.FrameWidth, l.FrameHeight: canvas dimensions
// - l.Blocks: map[string]Block with position data
// - l.RowOrders: node ordering per row
return []byte("..."), nil
}- Register in CLI in
internal/cli/render.go
See pkg/render/tower/sink for existing implementations.
Open an issue — we're happy to help!