Blend1D in blending.go hasn’t been changed in 363 days, and it is the oldest of five critical-band functions that have all gone between 150 and 363 days without a commit. Lipgloss has 544 functions in this snapshot, 37 of them in the critical band, and 107 of those 544 sit in the structural-debt quadrant versus just 2 in the fire quadrant. I read this as a maintenance backlog rather than an emergency: nothing here is churning today, but the highest-ranked function, queryTerminal in terminal.go (an activity-weighted risk score of 14.17, untouched for 184 days), and the 113-path Style.Render in style.go (untouched for 300 days) will be expensive for whoever has to change them next.
The table below ranks functions by activity-weighted risk — a score that multiplies structural complexity by recent commit frequency. A function that is both hard to understand (high cyclomatic complexity) and actively changing is a higher priority than one that is complex but untouched. CC = cyclomatic complexity (independent execution paths); ND = max nesting depth; FO = fan-out (distinct callees).
Top 5 Hotspots
| Function | File | Risk | CC | ND | FO |
|---|---|---|---|---|---|
queryTerminal | terminal.go | 14.2 | 19 | 4 | 16 |
Render | style.go | 14.1 | 113 | 4 | 57 |
render | tree/renderer.go | 12.8 | 36 | 4 | 32 |
BackgroundColor | query.go | 12.7 | 20 | 3 | 6 |
Blend1D | blending.go | 12.4 | 23 | 3 | 8 |
544 functions analyzed
Calls an unusually large number of distinct functions (high fan-out), making it the structural centre of gravity for a subsystem.Stale Complex×4Stale Complex
High structural complexity but untouched for a long time — structural debt that will bite whoever opens it next.Complex Branching×3Complex Branching
High cyclomatic complexity — many independent execution paths, each a potential bug surface and required test case.Exit Heavy×3Exit Heavy
Multiple return or throw paths dispersed through the body — each exit needs separate test coverage.Long Function×2Long Function
Function body is too long to review in a single pass; likely contains multiple distinct responsibilities.
The distribution above is the main finding. Of 544 functions, 107 are complex but dormant, and only 2 are both complex and recently active. All five functions in my Top 5 table are in the debt quadrant, with zero touches in the last year and between 150 and 363 days since last change. That changes how I’d prioritize: none of these is a live regression risk today, but each has a large blast radius when someone next needs to modify it. The two fire-quadrant functions are context rather than the headline. I come back to them at the end.
queryTerminal — terminal.go
I would start with queryTerminal. It has the highest activity-weighted risk score in the set (14.17) and pairs moderate branching with a wide fan-out: CC 19, ND 4, FO 16. It hasn’t been changed in 184 days and has zero touches in the last year.
From the source excerpt, this function writes an escape-sequence query to an output stream and reads the terminal’s reply from an input stream, with a timeout. To do that, it launches a goroutine that cancels the reader after the timeout, accumulates partial responses, decodes ANSI sequences in a loop, and hands completed sequences to a caller-supplied filter. A read loop wraps a second loop that consumes decoded sequences, and that is where the nesting depth of 4 comes from.
The structural metrics undersell the risk here. A goroutine plus a channel plus a cancellable reader is concurrency, and cyclomatic complexity doesn’t capture timing behavior. The error paths are explicit too: three separate wrapped-error returns plus a filter-driven early return give the function four distinct exits, which is a test-coverage burden, especially for the timeout path that is hard to exercise deterministically.
File-level history is thin. There are only 4 commits on terminal.go in total, 0 of them linked to bugs, no reverts, and a bug-fix rate of 50%, which on four commits is two fixes. That is too small a sample to read much into. I’d split the function into three pieces: the timeout/cancel setup, the write-query step, and the sequence-accumulation loop. The loop is the part that most needs unit tests, and it can be tested without any goroutine if it takes a reader and a filter.
Render — style.go
Style.Render is the most extreme function in the snapshot on raw structure. A cyclomatic complexity of 113 is past what I’d call extreme (the usual guide treats 100+ that way), and a fan-out of 57 means it calls 57 distinct functions. It hasn’t been changed in 300 days and has zero touches in the last year, so this is dormant debt with a very high blast radius.
The excerpt shows why the number is so large. The function opens by reading dozens of style properties (bold, italic, colors, padding, borders, alignment, transforms, hyperlinks) into local variables, then works through a long sequence of conditional blocks that apply each one. Nesting is shallow at 4, so the difficulty is breadth rather than depth: a long flat run of independent decisions. That fits the long-function and god-function patterns flagged here, and the stale-complex pattern says it has been complex and still for a while.
The file-level history is the most informative of the five. style.go has 23 total commits, a bug-fix rate of 43%, one revert, and a pull-request review comment density of 4.0, the highest in the set. None of the commits are tagged as bug-linked, and these are file-level numbers, so I wouldn’t say Render itself has caused defects. Still, a complex function in a file with a revert and heavy review discussion is where I’d want the strongest test coverage before touching anything.
I’d go with extract-method along the lines the function already draws for itself. The property-gathering preamble could become a small struct built by one helper. Text-attribute styling (bold, italic, underline, reverse, blink) and layout (padding, alignment, width, height, borders) are separate stages and could be separate functions. Because the function is so wide, I’d write characterization tests for the output across property combinations first, then extract one stage at a time.
render — tree/renderer.go
The tree renderer’s render method has CC 36, ND 4, and FO 32, and it hasn’t been changed in 296 days. From the excerpt, it does several jobs in one body. It renders an optional root label with padding, makes a first pass over children to compute the widest enumerator prefix (and prunes a hidden trailing child), then makes a second pass that styles each child’s indent, enumerator, and item. A pair of loops equalizes heights for multi-line prefixes by repeatedly joining vertically.
The two passes and the height-equalizing loops are natural seams. The first pass is a pure computation (widest prefix) that could be its own function, and the multi-line prefix alignment could be another. Splitting them would cut the complexity substantially and make each testable on its own. The file-level history has 6 commits, none bug-linked, no reverts, a bug-fix rate of 33%, and a review comment density of 3.0. I’d frame this as maintainability debt, not a defect signal. It is also a god-function case: a change in how prefixes are measured would ripple through both passes.
BackgroundColor — query.go
BackgroundColor is a different kind of hotspot. Its fan-out is only 6 and nesting is 3, so the score comes from CC 20, driven by a platform branch. The excerpt shows a Windows path that reopens the console explicitly when input or output is redirected, and a Unix path that requires a terminal and tries the input and output handles in turn. It has been untouched for 150 days, the most recent change among the five.
The flagged pattern is exit-heavy: many return points, most of them error returns. Each is a test case, and the Windows branches can only be exercised on Windows. The file has 3 commits with a bug-fix rate of 67%, which is two fixes out of three and again too small a sample to conclude anything, with no reverts. I’d separate the Windows console-opening logic into its own function, so the shared path becomes a short function that is simple to test on any platform and the platform-specific part is isolated.
Blend1D — blending.go
Blend1D is the oldest of the five at 363 days since last change, which makes it the clearest example of dormant debt in this set. CC is 23, ND is 3, and FO is 8. The excerpt shows a function that interpolates colors across a number of steps and stops, with several input-validation and early-return cases before the main work (negative steps, fewer steps than stops, nil stops, zero or one valid stop). The main loop then distributes steps across segments and blends each in the CIELAB color space.
That shape explains the exit-heavy and stale-complex flags: the edge cases account for most of the exits, and the interpolation core is a nested loop. The file’s history is quiet, with 3 commits, a bug-fix rate of 0%, no reverts, and no review comments. This is the lowest-concern file of the five, and I’d treat it as maintainability debt only. A reasonable first step is to move the guard clauses into a small normalization helper that returns the cleaned stop list, leaving the segment-distribution loop as the only body of Blend1D. The same file also holds a 2D variant, which I’d expect to share that helper.
What the fire quadrant adds
Two functions are in the fire quadrant, and I’d keep them on a watch list even though they aren’t in the Top 5. Both were changed recently, with one touch each in the last year. They are the only complex code anyone is currently editing, so a regression is more likely to land there first.
Patterns Found
Antipatterns detected across the top functions in this snapshot:
| Pattern | Occurrences |
|---|---|
god_function | 4 |
stale_complex | 4 |
complex_branching | 3 |
exit_heavy | 3 |
long_function | 2 |
These labels belong to two tiers — Tier 1 (structural): complex_branching, deeply_nested, exit_heavy, long_function, god_function. Tier 2 (relational/temporal): hub_function, cyclic_hub, middle_man, neighbor_risk, stale_complex, churn_magnet, shotgun_target, volatile_god.
See more analyses with these patterns: complex_branching, exit_heavy, god_function, long_function, stale_complex.
Reproduce This Analysis
git clone https://github.com/charmbracelet/lipgloss
cd lipgloss
git checkout 6a419c6543d3475a369ef08f6252a2a6f33be733
hotspots analyze . --mode snapshot --explain-patterns --force
To run the same analysis on your own codebase, run hotspots analyze . --mode snapshot in any local git repo — no configuration required.
I use Hotspots to highlight structural and activity risk — not “bad code.” I treat these findings as a prioritization aid, not a bug predictor. Editorial policy →