diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 53b4f62..4df2ba9 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -2,7 +2,8 @@ name: CI
on:
push:
- branches: [main]
+ branches-ignore:
+ - "dependabot/**" # Skip dependabot branches (they create PRs)
pull_request:
concurrency:
@@ -21,18 +22,19 @@ jobs:
- uses: actions/setup-go@v5
with:
- go-version-file: 'go.mod'
+ go-version-file: "go.mod"
cache: true
- name: Lint
uses: golangci/golangci-lint-action@v7
with:
- version: v2.1.6
+ version: v2.8.0
- name: Test
run: go test -race -coverprofile=coverage.txt -timeout=2m ./...
- name: Upload coverage to Codecov
+ if: github.ref == 'refs/heads/main'
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 7e786e3..2894db6 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -3,7 +3,7 @@ name: Release
on:
push:
tags:
- - 'v*'
+ - "v*"
workflow_dispatch:
# Manual trigger for re-running failed releases (requires existing tag)
@@ -20,7 +20,7 @@ jobs:
- uses: actions/setup-go@v5
with:
- go-version-file: 'go.mod'
+ go-version-file: "go.mod"
cache: true
- name: Run GoReleaser
diff --git a/.gitignore b/.gitignore
index e887019..863ce65 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,6 +11,9 @@
# Test binary, built with `go test -c`
*.test
+# GoReleaser output
+dist/
+
# Output of the go coverage tool
*.out
coverage.out
diff --git a/.goreleaser.yaml b/.goreleaser.yaml
index 79dc6b1..fc5e2f6 100644
--- a/.goreleaser.yaml
+++ b/.goreleaser.yaml
@@ -11,8 +11,9 @@ before:
- go test ./...
builds:
+ # CLI binary (stacktower) - for end users
- id: stacktower
- main: ./main.go
+ main: ./cmd/stacktower
binary: stacktower
env:
- CGO_ENABLED=0
@@ -25,12 +26,15 @@ builds:
- arm64
ldflags:
- -s -w
- - -X main.version={{.Version}}
- - -X main.commit={{.Commit}}
- - -X main.date={{.Date}}
+ - -X github.com/matzehuels/stacktower/pkg/buildinfo.Version={{.Version}}
+ - -X github.com/matzehuels/stacktower/pkg/buildinfo.Commit={{.Commit}}
+ - -X github.com/matzehuels/stacktower/pkg/buildinfo.Date={{.Date}}
archives:
+ # CLI archive (for Homebrew, manual download)
- id: stacktower
+ builds:
+ - stacktower
formats:
- tar.gz
name_template: >-
@@ -50,7 +54,7 @@ archives:
- README.md
checksum:
- name_template: 'checksums.txt'
+ name_template: "checksums.txt"
snapshot:
version_template: "{{ incpatch .Version }}-next"
@@ -60,11 +64,11 @@ changelog:
use: github
filters:
exclude:
- - '^docs:'
- - '^test:'
- - '^chore:'
- - '^ci:'
- - 'README'
+ - "^docs:"
+ - "^test:"
+ - "^chore:"
+ - "^ci:"
+ - "README"
- Merge pull request
- Merge branch
groups:
diff --git a/Makefile b/Makefile
index 1df93cc..ace9c06 100644
--- a/Makefile
+++ b/Makefile
@@ -1,14 +1,53 @@
-.PHONY: all build clean fmt lint test cover e2e e2e-test e2e-real e2e-parse blog blog-diagrams blog-showcase install-tools snapshot release help
+.PHONY: all build clean fmt fmt-check lint test cover vuln e2e blog install-tools snapshot release help
+
+# =============================================================================
+# Variables
+# =============================================================================
BINARY := stacktower
+VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
+COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown")
+DATE := $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
+LDFLAGS := -X github.com/matzehuels/stacktower/pkg/buildinfo.Version=$(VERSION) \
+ -X github.com/matzehuels/stacktower/pkg/buildinfo.Commit=$(COMMIT) \
+ -X github.com/matzehuels/stacktower/pkg/buildinfo.Date=$(DATE)
+
+# =============================================================================
+# Default Target
+# =============================================================================
all: check build
+# =============================================================================
+# Build Targets
+# =============================================================================
+
+build:
+ @echo "Building CLI (bin/$(BINARY))..."
+ @go build -ldflags "$(LDFLAGS)" -o bin/$(BINARY) ./cmd/stacktower
+
+install:
+ @echo "Installing CLI..."
+ @go install -ldflags "$(LDFLAGS)" ./cmd/stacktower
+
+clean:
+ @rm -rf bin/ dist/ coverage.out output/ tmp/
+
+# =============================================================================
+# Quality Checks
+# =============================================================================
+
check: fmt lint test vuln
fmt:
@gofmt -s -w .
- @goimports -w -local stacktower .
+ @goimports -w -local github.com/matzehuels/stacktower .
+
+fmt-check:
+ @echo "Checking formatting..."
+ @test -z "$$(gofmt -l .)" || (echo "Files not formatted:"; gofmt -l .; exit 1)
+ @test -z "$$(goimports -l -local github.com/matzehuels/stacktower .)" || (echo "Imports not formatted:"; goimports -l -local github.com/matzehuels/stacktower .; exit 1)
+ @echo "Formatting OK"
lint:
@golangci-lint run
@@ -20,23 +59,33 @@ cover:
@go test -race -coverprofile=coverage.out ./...
@go tool cover -func=coverage.out
-build:
- @go build -o bin/$(BINARY) .
+vuln:
+ @govulncheck ./...
-install:
- @go install .
+install-tools:
+ @go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
+ @go install golang.org/x/tools/cmd/goimports@latest
+ @go install golang.org/x/vuln/cmd/govulncheck@latest
+
+# =============================================================================
+# End-to-End Tests
+# =============================================================================
e2e: build
- @./scripts/test_e2e.sh all
+ @./scripts/test_cli_e2e.sh all
e2e-test: build
- @./scripts/test_e2e.sh test
+ @./scripts/test_cli_e2e.sh test
e2e-real: build
- @./scripts/test_e2e.sh real
+ @./scripts/test_cli_e2e.sh real
e2e-parse: build
- @./scripts/test_e2e.sh parse
+ @./scripts/test_cli_e2e.sh parse
+
+# =============================================================================
+# Blog Assets
+# =============================================================================
blog: blog-diagrams blog-showcase
@@ -46,13 +95,9 @@ blog-diagrams: build
blog-showcase: build
@./scripts/blog_showcase.sh
-install-tools:
- @go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
- @go install golang.org/x/tools/cmd/goimports@latest
- @go install golang.org/x/vuln/cmd/govulncheck@latest
-
-vuln:
- @govulncheck ./...
+# =============================================================================
+# Release
+# =============================================================================
snapshot:
@goreleaser release --snapshot --clean --skip=publish
@@ -60,23 +105,42 @@ snapshot:
release:
@goreleaser release --clean
-clean:
- @rm -rf bin/ dist/ coverage.out
+# =============================================================================
+# Help
+# =============================================================================
help:
- @echo "make - Run checks and build"
- @echo "make check - Format, lint, test, vulncheck (same as CI)"
- @echo "make fmt - Format code"
- @echo "make lint - Run golangci-lint"
- @echo "make test - Run tests"
- @echo "make cover - Run tests with coverage"
- @echo "make build - Build binary"
- @echo "make e2e - Run all end-to-end tests"
- @echo "make e2e-test - Render examples/test/*.json"
- @echo "make e2e-real - Render examples/real/*.json"
- @echo "make e2e-parse - Parse packages to examples/real/"
- @echo "make blog - Generate all blogpost diagrams"
- @echo "make blog-diagrams - Generate blogpost example diagrams"
- @echo "make blog-showcase - Generate blogpost showcase diagrams"
- @echo "make vuln - Check for vulnerabilities"
- @echo "make clean - Remove build artifacts"
+ @echo "Stacktower Makefile"
+ @echo ""
+ @echo "BUILDING:"
+ @echo " make build - Build CLI binary (bin/stacktower)"
+ @echo " make install - Install CLI to GOPATH"
+ @echo ""
+ @echo "QUALITY:"
+ @echo " make check - Run all checks (fmt, lint, test, vuln)"
+ @echo " make fmt - Format code"
+ @echo " make fmt-check - Check formatting (CI-style, no writes)"
+ @echo " make lint - Run golangci-lint"
+ @echo " make test - Run tests"
+ @echo " make cover - Run tests with coverage"
+ @echo " make vuln - Check for vulnerabilities"
+ @echo ""
+ @echo "TESTING:"
+ @echo " make e2e - Run all CLI end-to-end tests"
+ @echo " make e2e-test - Run test examples"
+ @echo " make e2e-real - Run real package examples"
+ @echo " make e2e-parse - Run parse tests"
+ @echo ""
+ @echo "BLOG:"
+ @echo " make blog - Generate all blog assets"
+ @echo " make blog-diagrams - Generate blog diagrams"
+ @echo " make blog-showcase - Generate blog showcase"
+ @echo ""
+ @echo "RELEASE:"
+ @echo " make snapshot - Build release locally (no publish)"
+ @echo " make release - Build and publish release"
+ @echo ""
+ @echo "OTHER:"
+ @echo " make clean - Remove build artifacts"
+ @echo " make install-tools - Install development tools"
+ @echo " make help - Show this help"
diff --git a/README.md b/README.md
index e161774..b31a74e 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@
[](https://github.com/matzehuels/stacktower/releases)
[](https://opensource.org/licenses/Apache-2.0)
-Inspired by [XKCD #2347](https://xkcd.com/2347/), Stacktower renders dependency graphs as **physical towers** where blocks rest on what they depend on. Your application sits at the top, supported by libraries below—all the way down to that one critical package maintained by *some dude in Nebraska*.
+Inspired by [XKCD #2347](https://xkcd.com/2347/), Stacktower renders dependency graphs as **physical towers** where blocks rest on what they depend on. Your application sits at the top, supported by libraries below—all the way down to that one critical package maintained by _some dude in Nebraska_.
@@ -36,7 +36,7 @@ go install github.com/matzehuels/stacktower@latest
```bash
git clone https://github.com/matzehuels/stacktower.git
cd stacktower
-go build -o stacktower .
+go build -o bin/stacktower ./cmd/stacktower
```
## Quick Start
@@ -88,6 +88,7 @@ stacktower parse go examples/manifest/go.mod -o deps.json
When the argument exists on disk or matches a known manifest filename, Stacktower automatically parses it as a manifest.
The project name (root node) is auto-detected from the manifest or a sibling file:
+
- **Cargo.toml**: `[package].name`
- **go.mod**: `module` directive
- **package.json**: `name` field
@@ -103,27 +104,6 @@ stacktower parse python requirements.txt --name="my-project" -o deps.json
stacktower parse ruby Gemfile -n my-rails-app -o deps.json
```
-#### Explicit Mode
-
-Force registry or manifest parsing when auto-detection isn't enough:
-
-```bash
-# Force registry lookup
-stacktower parse python registry pypi fastapi
-stacktower parse java registry maven org.springframework:spring-core
-stacktower parse go registry goproxy github.com/spf13/cobra
-
-# Force manifest type
-stacktower parse python manifest poetry examples/manifest/poetry.lock
-stacktower parse python manifest requirements examples/manifest/requirements.txt
-stacktower parse rust manifest cargo examples/manifest/Cargo.toml
-stacktower parse javascript manifest package examples/manifest/package.json
-stacktower parse ruby manifest gemfile examples/manifest/Gemfile
-stacktower parse php manifest composer examples/manifest/composer.json
-stacktower parse java manifest pom examples/manifest/pom.xml
-stacktower parse go manifest gomod examples/manifest/go.mod
-```
-
#### Metadata Enrichment
By default, Stacktower enriches packages with GitHub metadata (stars, maintainers, last commit) for richer visualizations. Set `GITHUB_TOKEN` to enable this:
@@ -132,8 +112,8 @@ By default, Stacktower enriches packages with GitHub metadata (stars, maintainer
export GITHUB_TOKEN=your_token
stacktower parse python fastapi -o fastapi.json
-# Disable enrichment if you don't have a token
-stacktower parse python fastapi --enrich=false -o fastapi.json
+# Skip enrichment if you don't have a token
+stacktower parse python fastapi --skip-enrich -o fastapi.json
```
### Rendering
@@ -144,6 +124,14 @@ The `render` command generates visualizations from parsed JSON graphs:
stacktower render [flags]
```
+This is a shortcut that combines `layout` and `visualize` in one step. For more control, you can run them separately:
+
+```bash
+# Two-step workflow with intermediate layout
+stacktower layout examples/real/flask.json -o flask.layout.json
+stacktower visualize flask.layout.json -o flask.svg
+```
+
#### Visualization Types
```bash
@@ -155,9 +143,6 @@ stacktower render examples/real/serde.json --style simple --randomize=false --po
# Traditional node-link diagram (uses Graphviz DOT)
stacktower render examples/real/yargs.json -t nodelink -o yargs.svg
-
-# Multiple types at once (outputs flask_tower.svg, flask_nodelink.svg)
-stacktower render examples/real/flask.json -t tower,nodelink -o flask
```
#### Output Formats
@@ -177,18 +162,16 @@ stacktower render examples/real/flask.json -f png -o flask.png
# Multiple formats at once (outputs flask.svg, flask.json, flask.pdf)
stacktower render examples/real/flask.json -f svg,json,pdf -o flask
-
-# Combine multiple types and formats
-stacktower render examples/real/flask.json -t tower,nodelink -f svg,json
```
Output path behavior:
+
- **No `-o`**: Derives from input (`input.json` → `input.`)
- **Single format**: Uses exact path (`-o out.svg` → `out.svg`)
-- **Multiple formats**: Strips extension, adds format (`-o out.svg -f svg,json` → `out.svg`, `out.json`)
-- **Multiple types**: Adds type suffix (`-t tower,nodelink` → `out_tower.svg`, `out_nodelink.svg`)
+- **Multiple formats**: Strips extension, adds format (`-o out -f svg,json` → `out.svg`, `out.json`)
> **Note:** PDF and PNG output requires [librsvg](https://wiki.gnome.org/Projects/LibRsvg):
+>
> - macOS: `brew install librsvg`
> - Linux: `apt install librsvg2-bin`
@@ -213,51 +196,45 @@ stacktower render examples/test/diamond.json -o diamond.svg
### Global Options
-| Flag | Description |
-|------|-------------|
+| Flag | Description |
+| ----------------- | -------------------------------------------------------- |
| `-v`, `--verbose` | Enable debug logging (search space info, timing details) |
### Parse Options
-| Flag | Description |
-|------|-------------|
-| `-o`, `--output` | Output file (stdout if empty) |
-| `-n`, `--name` | Project name for manifest parsing (auto-detected from manifest if not set) |
-| `--max-depth N` | Maximum dependency depth (default: 10) |
-| `--max-nodes N` | Maximum packages to fetch (default: 5000) |
-| `--enrich` | Enrich with GitHub metadata (default: true, requires `GITHUB_TOKEN`) |
-| `--refresh` | Bypass cache |
+| Flag | Description |
+| ---------------- | -------------------------------------------------------------------------- |
+| `-o`, `--output` | Output file (stdout if empty) |
+| `-n`, `--name` | Project name for manifest parsing (auto-detected from manifest if not set) |
+| `--max-depth N` | Maximum dependency depth (default: 10) |
+| `--max-nodes N` | Maximum packages to fetch (default: 5000) |
+| `--skip-enrich` | Skip metadata enrichment (GitHub descriptions, etc.) |
+| `--no-cache` | Disable caching |
### Render Options
-| Flag | Description |
-|------|-------------|
-| `-o`, `--output` | Output file or base path for multiple types/formats |
-| `-t`, `--type` | Visualization type(s): `tower` (default), `nodelink` (comma-separated) |
-| `-f`, `--format` | Output format(s): `svg` (default), `json`, `pdf`, `png` (comma-separated) |
-| `--normalize` | Apply graph normalization: break cycles, remove transitive edges, assign layers, subdivide long edges (default: true) |
+| Flag | Description |
+| ---------------- | --------------------------------------------------------------------------------------------------------------------- |
+| `-o`, `--output` | Output file or base path for multiple formats |
+| `-t`, `--type` | Visualization type: `tower` (default), `nodelink` |
+| `-f`, `--format` | Output format(s): `svg` (default), `json`, `pdf`, `png` (comma-separated) |
+| `--normalize` | Apply graph normalization: break cycles, remove transitive edges, assign layers, subdivide long edges (default: true) |
#### Tower Options
-| Flag | Description |
-|------|-------------|
-| `--width N` | Frame width in pixels (default: 800) |
-| `--height N` | Frame height in pixels (default: 600) |
-| `--style handdrawn\|simple` | Visual style (default: handdrawn) |
-| `--randomize` | Vary block widths to visualize load-bearing structure (default: true) |
-| `--merge` | Merge subdivider blocks into continuous towers (default: true) |
-| `--popups` | Enable hover popups with package metadata (default: true) |
-| `--nebraska` | Show "Nebraska guy" maintainer ranking panel |
-| `--edges` | Show dependency edges as dashed lines |
-| `--ordering optimal\|barycentric` | Crossing minimization algorithm (default: optimal) |
-| `--ordering-timeout N` | Timeout for optimal search in seconds (default: 60) |
-| `--top-down` | Width flows from roots down; by default width flows from sinks up |
-
-#### Node-Link Options
-
-| Flag | Description |
-|------|-------------|
-| `--detailed` | Show all node metadata in labels |
+| Flag | Description |
+| --------------------------------- | --------------------------------------------------------------------- |
+| `--width N` | Frame width in pixels (default: 800) |
+| `--height N` | Frame height in pixels (default: 600) |
+| `--style handdrawn\|simple` | Visual style (default: handdrawn) |
+| `--randomize` | Vary block widths to visualize load-bearing structure (default: true) |
+| `--merge` | Merge subdivider blocks into continuous towers (default: true) |
+| `--popups` | Enable hover popups with package metadata (default: true) |
+| `--nebraska` | Show "Nebraska guy" maintainer ranking panel |
+| `--edges` | Show dependency edges as dashed lines |
+| `--ordering optimal\|barycentric` | Crossing minimization algorithm (default: optimal) |
+| `--ordering-timeout N` | Timeout for optimal search in seconds (default: 60) |
+| `--no-cache` | Disable caching |
## JSON Format
@@ -267,11 +244,7 @@ The render layer accepts a simple JSON format, making it easy to visualize **any
```json
{
- "nodes": [
- { "id": "app" },
- { "id": "lib-a" },
- { "id": "lib-b" }
- ],
+ "nodes": [{ "id": "app" }, { "id": "lib-a" }, { "id": "lib-b" }],
"edges": [
{ "from": "app", "to": "lib-a" },
{ "from": "lib-a", "to": "lib-b" }
@@ -281,34 +254,34 @@ The render layer accepts a simple JSON format, making it easy to visualize **any
### Required Fields
-| Field | Type | Description |
-|-------|------|-------------|
-| `nodes[].id` | string | Unique node identifier (displayed as label) |
-| `edges[].from` | string | Source node ID |
-| `edges[].to` | string | Target node ID |
+| Field | Type | Description |
+| -------------- | ------ | ------------------------------------------- |
+| `nodes[].id` | string | Unique node identifier (displayed as label) |
+| `edges[].from` | string | Source node ID |
+| `edges[].to` | string | Target node ID |
### Optional Fields
-| Field | Type | Description |
-|-------|------|-------------|
-| `nodes[].row` | int | Pre-assigned layer (computed automatically if omitted) |
-| `nodes[].kind` | string | Internal use: `"subdivider"` or `"auxiliary"` |
-| `nodes[].meta` | object | Freeform metadata for display features |
+| Field | Type | Description |
+| -------------- | ------ | ------------------------------------------------------ |
+| `nodes[].row` | int | Pre-assigned layer (computed automatically if omitted) |
+| `nodes[].kind` | string | Internal use: `"subdivider"` or `"auxiliary"` |
+| `nodes[].meta` | object | Freeform metadata for display features |
### Recognized `meta` Keys
These keys are read by specific render flags. All are optional—missing keys simply disable the corresponding feature.
-| Key | Type | Used By |
-|-----|------|---------|
-| `repo_url` | string | Clickable blocks, `--popups`, `--nebraska` |
-| `repo_stars` | int | `--popups` |
-| `repo_owner` | string | `--nebraska` |
-| `repo_maintainers` | []string | `--nebraska` |
-| `repo_last_commit` | string (date) | `--popups`, brittle detection |
-| `repo_last_release` | string (date) | `--popups` |
-| `repo_archived` | bool | `--popups`, brittle detection |
-| `summary` | string | `--popups` (fallback: `description`) |
+| Key | Type | Used By |
+| ------------------- | ------------- | ------------------------------------------ |
+| `repo_url` | string | Clickable blocks, `--popups`, `--nebraska` |
+| `repo_stars` | int | `--popups` |
+| `repo_owner` | string | `--nebraska` |
+| `repo_maintainers` | []string | `--nebraska` |
+| `repo_last_commit` | string (date) | `--popups`, brittle detection |
+| `repo_last_release` | string (date) | `--popups` |
+| `repo_archived` | bool | `--popups`, brittle detection |
+| `summary` | string | `--popups` (fallback: `description`) |
The `--detailed` flag (node-link only) displays **all** meta keys in the node label.
@@ -323,16 +296,34 @@ The `--detailed` flag (node-link only) displays **all** meta keys in the node la
The ordering step is where the magic happens. Stacktower uses an optimal search algorithm that guarantees minimum crossings for small-to-medium graphs. For larger graphs, it gracefully falls back after a configurable timeout.
+## GitHub Authentication
+
+For parsing repositories directly from GitHub, you can authenticate using the device flow:
+
+```bash
+# Login with GitHub (opens browser for device authorization)
+stacktower github login
+
+# Check current session
+stacktower github whoami
+
+# Logout
+stacktower github logout
+
+# Parse a manifest from a GitHub repository
+stacktower parse github owner/repo -o deps.json
+```
+
## Environment Variables
-| Variable | Description |
-|----------|-------------|
-| `GITHUB_TOKEN` | GitHub API token for `--enrich` metadata |
-| `GITLAB_TOKEN` | GitLab API token for `--enrich` metadata |
+| Variable | Description |
+| -------------- | ------------------------------------------------ |
+| `GITHUB_TOKEN` | GitHub API token for metadata enrichment |
+| `GITLAB_TOKEN` | GitLab API token for metadata enrichment |
## Caching
-HTTP responses are cached in `~/.cache/stacktower/` with a 24-hour TTL. Use `--refresh` to bypass the cache for a single request.
+HTTP responses are cached in `~/.cache/stacktower/` with a 24-hour TTL. Use `--no-cache` to disable caching for a single request.
```bash
# Clear the entire cache
@@ -348,10 +339,10 @@ Stacktower can be used as a Go library for programmatic graph visualization.
```go
import (
- "github.com/matzehuels/stacktower/pkg/dag"
- "github.com/matzehuels/stacktower/pkg/dag/transform"
- "github.com/matzehuels/stacktower/pkg/render/tower/layout"
- "github.com/matzehuels/stacktower/pkg/render/tower/sink"
+ "github.com/matzehuels/stacktower/pkg/core/dag"
+ "github.com/matzehuels/stacktower/pkg/core/dag/transform"
+ "github.com/matzehuels/stacktower/pkg/core/render/tower/layout"
+ "github.com/matzehuels/stacktower/pkg/core/render/tower/sink"
)
// Build a graph
@@ -369,10 +360,12 @@ svg := sink.RenderSVG(l, sink.WithGraph(g), sink.WithPopups())
📚 **[Full API documentation on pkg.go.dev](https://pkg.go.dev/github.com/matzehuels/stacktower)**
Key packages:
-- [`pkg/dag`](https://pkg.go.dev/github.com/matzehuels/stacktower/pkg/dag) — DAG data structure and crossing algorithms
-- [`pkg/dag/transform`](https://pkg.go.dev/github.com/matzehuels/stacktower/pkg/dag/transform) — Graph normalization pipeline
-- [`pkg/render/tower`](https://pkg.go.dev/github.com/matzehuels/stacktower/pkg/render/tower) — Layout, ordering, and rendering
-- [`pkg/deps`](https://pkg.go.dev/github.com/matzehuels/stacktower/pkg/deps) — Dependency resolution from registries
+
+- [`pkg/core/dag`](https://pkg.go.dev/github.com/matzehuels/stacktower/pkg/core/dag) — DAG data structure and crossing algorithms
+- [`pkg/core/dag/transform`](https://pkg.go.dev/github.com/matzehuels/stacktower/pkg/core/dag/transform) — Graph normalization pipeline
+- [`pkg/core/render/tower`](https://pkg.go.dev/github.com/matzehuels/stacktower/pkg/core/render/tower) — Layout, ordering, and rendering
+- [`pkg/core/deps`](https://pkg.go.dev/github.com/matzehuels/stacktower/pkg/core/deps) — Dependency resolution from registries
+- [`pkg/pipeline`](https://pkg.go.dev/github.com/matzehuels/stacktower/pkg/pipeline) — Complete parse → layout → render pipeline
## Contributing
@@ -386,16 +379,16 @@ make check # Run all CI checks locally (fmt, lint, test, vuln)
make build # Build binary to bin/stacktower
```
-| Command | Description |
-|---------|-------------|
-| `make check` | Format, lint, test, vulncheck (same as CI) |
-| `make fmt` | Format code with gofmt and goimports |
-| `make lint` | Run golangci-lint |
-| `make test` | Run tests with race detector |
-| `make cover` | Run tests with coverage report |
-| `make vuln` | Check for known vulnerabilities |
-| `make e2e` | Run end-to-end tests |
-| `make snapshot` | Build release locally (no publish) |
+| Command | Description |
+| --------------- | ------------------------------------------ |
+| `make check` | Format, lint, test, vulncheck (same as CI) |
+| `make fmt` | Format code with gofmt and goimports |
+| `make lint` | Run golangci-lint |
+| `make test` | Run tests with race detector |
+| `make cover` | Run tests with coverage report |
+| `make vuln` | Check for known vulnerabilities |
+| `make e2e` | Run end-to-end tests |
+| `make snapshot` | Build release locally (no publish) |
Commit messages follow [Conventional Commits](https://www.conventionalcommits.org/).
diff --git a/blogpost/android-chrome-192x192.png b/blogpost/android-chrome-192x192.png
index 2706f78..b842284 100644
Binary files a/blogpost/android-chrome-192x192.png and b/blogpost/android-chrome-192x192.png differ
diff --git a/blogpost/android-chrome-512x512.png b/blogpost/android-chrome-512x512.png
index 2f2243e..025ccdb 100644
Binary files a/blogpost/android-chrome-512x512.png and b/blogpost/android-chrome-512x512.png differ
diff --git a/blogpost/apple-touch-icon.png b/blogpost/apple-touch-icon.png
index 3aaa75c..571ab67 100644
Binary files a/blogpost/apple-touch-icon.png and b/blogpost/apple-touch-icon.png differ
diff --git a/blogpost/favicon-16x16.png b/blogpost/favicon-16x16.png
index 5844882..6c43ad9 100644
Binary files a/blogpost/favicon-16x16.png and b/blogpost/favicon-16x16.png differ
diff --git a/blogpost/favicon-32x32.png b/blogpost/favicon-32x32.png
index bf7dc7d..74bf318 100644
Binary files a/blogpost/favicon-32x32.png and b/blogpost/favicon-32x32.png differ
diff --git a/blogpost/favicon.ico b/blogpost/favicon.ico
index 9f9dd82..e6cbece 100644
Binary files a/blogpost/favicon.ico and b/blogpost/favicon.ico differ
diff --git a/blogpost/plots/showcase/javascript/ioredis.svg b/blogpost/plots/showcase/javascript/ioredis.svg
index 561daab..0e706e2 100644
--- a/blogpost/plots/showcase/javascript/ioredis.svg
+++ b/blogpost/plots/showcase/javascript/ioredis.svg
@@ -1,13 +1,19 @@
-