Skip to content

Commit d658c8f

Browse files
committed
refactor(tower): reorganize package into feature, layout, and sink subpackages
Restructure pkg/render/tower/ for better separation of concerns: - pkg/render/tower/feature/ - brittle detection, Nebraska ranking - pkg/render/tower/layout/ - block positioning, width computation - pkg/render/tower/sink/ - output formats (SVG, JSON, PDF, PNG) - pkg/render/tower/styles/ - visual rendering (handdrawn, simple) - pkg/render/tower/transform/ - layout transforms (merge, randomize) This enables cleaner imports and makes it easier to add new output formats.
1 parent 20e0551 commit d658c8f

26 files changed

+563
-569
lines changed

pkg/render/tower/block.go

Lines changed: 0 additions & 32 deletions
This file was deleted.
Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package tower
1+
package feature
22

33
import (
44
"time"
@@ -21,7 +21,7 @@ func IsBrittle(n *dag.Node) bool {
2121
return true
2222
}
2323

24-
lastCommit := parseDate(n.Meta["repo_last_commit"])
24+
lastCommit := ParseDate(n.Meta["repo_last_commit"])
2525
if lastCommit.IsZero() {
2626
return false
2727
}
@@ -34,12 +34,12 @@ func IsBrittle(n *dag.Node) bool {
3434
return false
3535
}
3636

37-
maintainers := countMaintainers(n.Meta["repo_maintainers"])
37+
maintainers := CountMaintainers(n.Meta["repo_maintainers"])
3838
stars, _ := n.Meta["repo_stars"].(int)
3939
return maintainers == 1 || stars < lowStarCount || maintainers <= minMaintainerCount
4040
}
4141

42-
func parseDate(v any) time.Time {
42+
func ParseDate(v any) time.Time {
4343
s, ok := v.(string)
4444
if !ok || s == "" {
4545
return time.Time{}
@@ -48,7 +48,7 @@ func parseDate(v any) time.Time {
4848
return t
4949
}
5050

51-
func countMaintainers(v any) int {
51+
func CountMaintainers(v any) int {
5252
switch v := v.(type) {
5353
case []string:
5454
return len(v)
@@ -57,3 +57,13 @@ func countMaintainers(v any) int {
5757
}
5858
return 0
5959
}
60+
61+
func AsInt(v any) int {
62+
switch v := v.(type) {
63+
case int:
64+
return v
65+
case float64:
66+
return int(v)
67+
}
68+
return 0
69+
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package tower
1+
package feature
22

33
import (
44
"testing"
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
package feature
2+
3+
import (
4+
"cmp"
5+
"slices"
6+
7+
"github.com/matzehuels/stacktower/pkg/dag"
8+
)
9+
10+
type Role string
11+
12+
const (
13+
RoleOwner Role = "owner"
14+
RoleLead Role = "lead"
15+
RoleMaintainer Role = "maintainer"
16+
)
17+
18+
type PackageRole struct {
19+
Package string
20+
Role Role
21+
URL string
22+
Depth int
23+
}
24+
25+
type NebraskaRanking struct {
26+
Maintainer string
27+
Score float64
28+
Packages []PackageRole
29+
}
30+
31+
const (
32+
ownerWeight = 3.0
33+
leadWeight = 1.5
34+
maintainerWeight = 1.0
35+
)
36+
37+
func RankNebraska(g *dag.DAG, topN int) []NebraskaRanking {
38+
scores := make(map[string]float64)
39+
packages := make(map[string][]PackageRole)
40+
bestRole := make(map[string]Role)
41+
minRow := findMinRow(g)
42+
43+
for _, n := range g.Nodes() {
44+
if n.IsSynthetic() || g.InDegree(n.ID) == 0 {
45+
continue
46+
}
47+
48+
roles := getMaintainerRoles(n)
49+
if len(roles) == 0 {
50+
continue
51+
}
52+
53+
depth := n.Row - minRow
54+
share := float64(depth) / float64(len(roles))
55+
56+
for maintainer, role := range roles {
57+
scores[maintainer] += share * roleWeight(role)
58+
59+
if !hasPackage(packages[maintainer], n.ID) {
60+
url, _ := n.Meta["repo_url"].(string)
61+
packages[maintainer] = append(packages[maintainer], PackageRole{
62+
Package: n.ID,
63+
Role: role,
64+
URL: url,
65+
Depth: depth,
66+
})
67+
}
68+
69+
if roleRank(role) < roleRank(bestRole[maintainer]) {
70+
bestRole[maintainer] = role
71+
}
72+
}
73+
}
74+
75+
rankings := make([]NebraskaRanking, 0, len(scores))
76+
for m, score := range scores {
77+
pkgs := packages[m]
78+
slices.SortFunc(pkgs, func(a, b PackageRole) int {
79+
if c := cmp.Compare(roleRank(a.Role), roleRank(b.Role)); c != 0 {
80+
return c
81+
}
82+
if c := cmp.Compare(b.Depth, a.Depth); c != 0 {
83+
return c
84+
}
85+
return cmp.Compare(a.Package, b.Package)
86+
})
87+
rankings = append(rankings, NebraskaRanking{
88+
Maintainer: m,
89+
Score: score,
90+
Packages: pkgs,
91+
})
92+
}
93+
94+
slices.SortFunc(rankings, func(a, b NebraskaRanking) int {
95+
if c := cmp.Compare(b.Score, a.Score); c != 0 {
96+
return c
97+
}
98+
if c := cmp.Compare(roleRank(bestRole[a.Maintainer]), roleRank(bestRole[b.Maintainer])); c != 0 {
99+
return c
100+
}
101+
return cmp.Compare(a.Maintainer, b.Maintainer)
102+
})
103+
104+
if len(rankings) > topN {
105+
return rankings[:topN]
106+
}
107+
return rankings
108+
}
109+
110+
func roleRank(r Role) int {
111+
switch r {
112+
case RoleOwner:
113+
return 0
114+
case RoleLead:
115+
return 1
116+
case RoleMaintainer:
117+
return 2
118+
default:
119+
return 3
120+
}
121+
}
122+
123+
func roleWeight(r Role) float64 {
124+
switch r {
125+
case RoleOwner:
126+
return ownerWeight
127+
case RoleLead:
128+
return leadWeight
129+
default:
130+
return maintainerWeight
131+
}
132+
}
133+
134+
func findMinRow(g *dag.DAG) int {
135+
minRow := -1
136+
for _, n := range g.Nodes() {
137+
if !n.IsSynthetic() && (minRow < 0 || n.Row < minRow) {
138+
minRow = n.Row
139+
}
140+
}
141+
return max(0, minRow)
142+
}
143+
144+
func getMaintainerRoles(n *dag.Node) map[string]Role {
145+
if n.Meta == nil {
146+
return nil
147+
}
148+
149+
owner, _ := n.Meta["repo_owner"].(string)
150+
maintainers := getStringSlice(n.Meta["repo_maintainers"])
151+
152+
if len(maintainers) == 0 && owner != "" {
153+
return map[string]Role{owner: RoleOwner}
154+
}
155+
156+
roles := make(map[string]Role, len(maintainers))
157+
leadAssigned := false
158+
159+
for _, m := range maintainers {
160+
switch {
161+
case m == owner:
162+
roles[m] = RoleOwner
163+
case !leadAssigned:
164+
roles[m] = RoleLead
165+
leadAssigned = true
166+
default:
167+
roles[m] = RoleMaintainer
168+
}
169+
}
170+
return roles
171+
}
172+
173+
func getStringSlice(v any) []string {
174+
switch v := v.(type) {
175+
case []string:
176+
return v
177+
case []any:
178+
out := make([]string, 0, len(v))
179+
for _, item := range v {
180+
if s, ok := item.(string); ok {
181+
out = append(out, s)
182+
}
183+
}
184+
return out
185+
}
186+
return nil
187+
}
188+
189+
func hasPackage(pkgs []PackageRole, id string) bool {
190+
return slices.ContainsFunc(pkgs, func(p PackageRole) bool { return p.Package == id })
191+
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package tower
1+
package feature
22

33
import (
44
"testing"

pkg/render/tower/json.go

Lines changed: 0 additions & 11 deletions
This file was deleted.

pkg/render/tower/layout/block.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package layout
2+
3+
type Block struct {
4+
NodeID string
5+
Left, Right float64
6+
Bottom, Top float64
7+
}
8+
9+
func (b Block) Width() float64 { return b.Right - b.Left }
10+
func (b Block) Height() float64 { return b.Top - b.Bottom }
11+
func (b Block) CenterX() float64 { return (b.Left + b.Right) / 2 }
12+
func (b Block) CenterY() float64 { return (b.Bottom + b.Top) / 2 }
Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package tower
1+
package layout
22

33
import (
44
"slices"
@@ -13,12 +13,12 @@ const (
1313
)
1414

1515
type Layout struct {
16-
FrameWidth float64 `json:"width"`
17-
FrameHeight float64 `json:"height"`
18-
Blocks map[string]Block `json:"blocks"`
19-
RowOrders map[int][]string `json:"rows,omitempty"`
20-
MarginX float64 `json:"margin_x"`
21-
MarginY float64 `json:"margin_y"`
16+
FrameWidth float64
17+
FrameHeight float64
18+
Blocks map[string]Block
19+
RowOrders map[int][]string
20+
MarginX float64
21+
MarginY float64
2222
}
2323

2424
type Option func(*config)
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package tower
1+
package layout
22

33
import (
44
"math"
Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package tower
1+
package layout
22

33
import (
44
"math"
@@ -68,22 +68,19 @@ func ComputeWidthsBottomUp(g *dag.DAG, orders map[int][]string, frameWidth float
6868
widths := make(map[string]float64, g.NodeCount())
6969
maxRow := rows[len(rows)-1]
7070

71-
// Start from bottom: sinks get equal width
7271
if bottomRow := orders[maxRow]; len(bottomRow) > 0 {
7372
unit := frameWidth / float64(len(bottomRow))
7473
for _, id := range bottomRow {
7574
widths[id] = unit
7675
}
7776
}
7877

79-
// Propagate upward: parent width = sum of children's contributions
8078
for r := maxRow - 1; r >= 0; r-- {
8179
currRow := orders[r]
8280
if len(currRow) == 0 {
8381
continue
8482
}
8583

86-
// Each parent gets width from its children
8784
for _, id := range currRow {
8885
widths[id] = 0.0
8986
}
@@ -93,8 +90,6 @@ func ComputeWidthsBottomUp(g *dag.DAG, orders map[int][]string, frameWidth float
9390
if len(kids) == 0 {
9491
continue
9592
}
96-
// Parent gets the sum of its share from each child
97-
// Each child divides its width among its parents
9893
for _, kid := range kids {
9994
parents := g.ParentsInRow(kid, r)
10095
if len(parents) > 0 {
@@ -103,7 +98,6 @@ func ComputeWidthsBottomUp(g *dag.DAG, orders map[int][]string, frameWidth float
10398
}
10499
}
105100

106-
// Normalize row to fill frame width
107101
var sum float64
108102
for _, id := range currRow {
109103
sum += widths[id]

0 commit comments

Comments
 (0)