Skip to content

Commit 78be48f

Browse files
fix: address review comments and resolve merge conflicts with main
- Rebase api/client.go and template/render.go onto main's revamped data structures (Subdomain struct, ExternalDeps, []string Languages) - Fix: preserve backend error messages in GetCircularDependencies default error case, matching GetGraph error handling parity - Fix: update GetCircularDependencies docstring to accurately describe nil,nil (unavailable) vs empty response (no cycles found) - Fix: pass circularCycles to truncateToTokenBudget so the warning is preserved even when output is token-truncated Co-authored-by: Grey Newell <greynewell@users.noreply.github.com>
1 parent dfc32a0 commit 78be48f

File tree

2 files changed

+122
-63
lines changed

2 files changed

+122
-63
lines changed

internal/api/client.go

Lines changed: 88 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -31,16 +31,20 @@ type Client struct {
3131

3232
// SupermodelIR is the raw response from the Supermodel API /v1/graphs/supermodel endpoint.
3333
type SupermodelIR struct {
34-
Repo string `json:"repo"`
35-
Summary irSummary `json:"summary"`
36-
Metadata irMetadata `json:"metadata"`
37-
Domains []irDomain `json:"domains"`
34+
Repo string `json:"repo"`
35+
Summary map[string]any `json:"summary"`
36+
Metadata irMetadata `json:"metadata"`
37+
Domains []irDomain `json:"domains"`
38+
Graph irGraph `json:"graph"`
3839
}
3940

40-
type irSummary struct {
41-
FilesProcessed int `json:"filesProcessed"`
42-
Functions int `json:"functions"`
43-
PrimaryLanguage *string `json:"primaryLanguage"`
41+
type irGraph struct {
42+
Nodes []irNode `json:"nodes"`
43+
}
44+
45+
type irNode struct {
46+
Type string `json:"type"`
47+
Name string `json:"name"`
4448
}
4549

4650
type irMetadata struct {
@@ -49,11 +53,11 @@ type irMetadata struct {
4953
}
5054

5155
type irDomain struct {
52-
Name string `json:"name"`
53-
DescriptionSummary string `json:"descriptionSummary"`
54-
KeyFiles []string `json:"keyFiles"`
55-
Responsibilities []string `json:"responsibilities"`
56-
Subdomains []irSubdomain `json:"subdomains"`
56+
Name string `json:"name"`
57+
DescriptionSummary string `json:"descriptionSummary"`
58+
KeyFiles []string `json:"keyFiles"`
59+
Responsibilities []string `json:"responsibilities"`
60+
Subdomains []irSubdomain `json:"subdomains"`
5761
}
5862

5963
type irSubdomain struct {
@@ -67,71 +71,96 @@ func (ir *SupermodelIR) toProjectGraph(projectName string) *ProjectGraph {
6771
if len(ir.Metadata.Languages) > 0 {
6872
lang = ir.Metadata.Languages[0]
6973
}
70-
if ir.Summary.PrimaryLanguage != nil && *ir.Summary.PrimaryLanguage != "" {
71-
lang = *ir.Summary.PrimaryLanguage
74+
if v, ok := ir.Summary["primaryLanguage"]; ok && v != nil {
75+
if s, ok := v.(string); ok && s != "" {
76+
lang = s
77+
}
7278
}
7379

74-
langMap := make(map[string]int, len(ir.Metadata.Languages))
75-
for _, l := range ir.Metadata.Languages {
76-
langMap[l] = 0 // count not available from API
80+
// Extract integer fields from the free-form summary map.
81+
// JSON numbers unmarshal as float64 in map[string]any.
82+
summaryInt := func(key string) int {
83+
if v, ok := ir.Summary[key]; ok {
84+
if n, ok := v.(float64); ok {
85+
return int(n)
86+
}
87+
}
88+
return 0
7789
}
7890

7991
domains := make([]Domain, 0, len(ir.Domains))
8092
for _, d := range ir.Domains {
81-
subdomainNames := make([]string, 0, len(d.Subdomains))
93+
subdomains := make([]Subdomain, 0, len(d.Subdomains))
8294
for _, s := range d.Subdomains {
83-
subdomainNames = append(subdomainNames, s.Name)
95+
subdomains = append(subdomains, Subdomain{
96+
Name: s.Name,
97+
Description: s.DescriptionSummary,
98+
})
8499
}
85100
domains = append(domains, Domain{
86101
Name: d.Name,
87102
Description: d.DescriptionSummary,
88103
KeyFiles: d.KeyFiles,
89104
Responsibilities: d.Responsibilities,
90-
Subdomains: subdomainNames,
105+
Subdomains: subdomains,
91106
})
92107
}
93108

109+
var externalDeps []string
110+
for _, node := range ir.Graph.Nodes {
111+
if node.Type == "ExternalDependency" && node.Name != "" {
112+
externalDeps = append(externalDeps, node.Name)
113+
}
114+
}
115+
94116
return &ProjectGraph{
95-
Name: projectName,
96-
Language: lang,
97-
Domains: domains,
117+
Name: projectName,
118+
Language: lang,
119+
Domains: domains,
120+
ExternalDeps: externalDeps,
98121
Stats: Stats{
99-
TotalFiles: ir.Summary.FilesProcessed,
100-
TotalFunctions: ir.Summary.Functions,
101-
Languages: langMap,
122+
TotalFiles: summaryInt("filesProcessed"),
123+
TotalFunctions: summaryInt("functions"),
124+
Languages: ir.Metadata.Languages,
102125
},
103126
UpdatedAt: time.Now(),
104127
}
105128
}
106129

107130
// ProjectGraph is the internal model used by the cache and template.
108131
type ProjectGraph struct {
109-
Name string `json:"name"`
110-
Language string `json:"language"`
111-
Framework string `json:"framework,omitempty"`
112-
Description string `json:"description,omitempty"`
113-
Domains []Domain `json:"domains"`
114-
Stats Stats `json:"stats"`
115-
UpdatedAt time.Time `json:"updated_at"`
132+
Name string `json:"name"`
133+
Language string `json:"language"`
134+
Framework string `json:"framework,omitempty"`
135+
Description string `json:"description,omitempty"`
136+
Domains []Domain `json:"domains"`
137+
ExternalDeps []string `json:"external_deps,omitempty"`
138+
Stats Stats `json:"stats"`
139+
UpdatedAt time.Time `json:"updated_at"`
140+
}
141+
142+
// Subdomain represents a named sub-area within a domain.
143+
type Subdomain struct {
144+
Name string `json:"name"`
145+
Description string `json:"description,omitempty"`
116146
}
117147

118148
// Domain represents a semantic domain within the project.
119149
type Domain struct {
120-
Name string `json:"name"`
121-
Description string `json:"description"`
122-
KeyFiles []string `json:"key_files"`
123-
Responsibilities []string `json:"responsibilities"`
124-
Subdomains []string `json:"subdomains,omitempty"`
125-
DependsOn []string `json:"depends_on,omitempty"`
150+
Name string `json:"name"`
151+
Description string `json:"description"`
152+
KeyFiles []string `json:"key_files"`
153+
Responsibilities []string `json:"responsibilities"`
154+
Subdomains []Subdomain `json:"subdomains,omitempty"`
155+
DependsOn []string `json:"depends_on,omitempty"`
126156
}
127157

128158
// Stats holds codebase statistics.
129159
type Stats struct {
130-
TotalFiles int `json:"total_files"`
131-
TotalFunctions int `json:"total_functions"`
132-
TotalLines int `json:"total_lines"`
133-
Languages map[string]int `json:"languages,omitempty"`
134-
CircularDependencyCycles int `json:"circular_dependency_cycles,omitempty"`
160+
TotalFiles int `json:"total_files"`
161+
TotalFunctions int `json:"total_functions"`
162+
Languages []string `json:"languages,omitempty"`
163+
CircularDependencyCycles int `json:"circular_dependency_cycles,omitempty"`
135164
}
136165

137166
// CircularDependencyCycle represents a single circular import chain.
@@ -292,7 +321,7 @@ func (c *Client) GetGraph(ctx context.Context, projectName string, repoZip []byt
292321

293322
// GetCircularDependencies submits the repo zip to the circular dependency endpoint
294323
// and returns the list of detected import cycles. Returns nil, nil if the endpoint
295-
// is unavailable or returns no cycles — callers should treat this as "no data".
324+
// is unavailable. If available but no cycles are found, returns an empty response.
296325
func (c *Client) GetCircularDependencies(ctx context.Context, projectName string, repoZip []byte) (*CircularDependencyResponse, error) {
297326
c.logFn("[debug] checking circular dependencies (%d bytes)", len(repoZip))
298327

@@ -359,7 +388,19 @@ func (c *Client) GetCircularDependencies(ctx context.Context, projectName string
359388
case http.StatusOK, http.StatusAccepted:
360389
// Continue to parse
361390
default:
362-
return nil, fmt.Errorf("API error %d", resp.StatusCode)
391+
var errResp struct {
392+
Message string `json:"message"`
393+
Error string `json:"error"`
394+
}
395+
_ = json.Unmarshal(respBody, &errResp)
396+
msg := errResp.Message
397+
if msg == "" {
398+
msg = errResp.Error
399+
}
400+
if msg == "" {
401+
msg = string(respBody)
402+
}
403+
return nil, fmt.Errorf("API error %d: %s", resp.StatusCode, msg)
363404
}
364405

365406
var jobResp JobStatus

internal/template/render.go

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,11 @@ const contextBombTmpl = `# Uncompact Context — {{.ProjectName}}
2424
**Language:** {{.Graph.Language}}{{if .Graph.Framework}}
2525
**Framework:** {{.Graph.Framework}}{{end}}{{if .Graph.Description}}
2626
**Description:** {{.Graph.Description}}{{end}}
27-
**Codebase:** {{.Graph.Stats.TotalFiles}} files · {{.Graph.Stats.TotalFunctions}} functions · {{.Graph.Stats.TotalLines}} lines
27+
**Codebase:** {{.Graph.Stats.TotalFiles}} files · {{.Graph.Stats.TotalFunctions}} functions
2828
{{- if .Graph.Stats.Languages}}
2929
30-
**Languages:** {{languageList .Graph.Stats.Languages}}{{end}}
30+
**Languages:** {{languageList .Graph.Stats.Languages}}{{end}}{{if .Graph.ExternalDeps}}
31+
**Tech stack:** {{join .Graph.ExternalDeps ", "}}{{end}}
3132
3233
## Domain Map
3334
{{range .Graph.Domains}}
@@ -36,8 +37,9 @@ const contextBombTmpl = `# Uncompact Context — {{.ProjectName}}
3637
{{if .KeyFiles}}**Key files:** {{join .KeyFiles ", "}}
3738
{{end}}{{if .Responsibilities}}**Responsibilities:**
3839
{{range .Responsibilities}}- {{.}}
39-
{{end}}{{end}}{{if .Subdomains}}**Subdomains:** {{join .Subdomains ", "}}
40-
{{end}}{{if .DependsOn}}**Depends on:** {{join .DependsOn ", "}}
40+
{{end}}{{end}}{{if .Subdomains}}**Subdomains:**
41+
{{range .Subdomains}}- {{.Name}}{{if .Description}}: {{.Description}}{{end}}
42+
{{end}}{{end}}{{if .DependsOn}}**Depends on:** {{join .DependsOn ", "}}
4143
{{end}}{{end}}
4244
---
4345
*Generated by [Uncompact](https://github.com/supermodeltools/Uncompact)*`
@@ -76,12 +78,8 @@ func Render(graph *api.ProjectGraph, projectName string, opts RenderOptions) (st
7678

7779
funcMap := gotmpl.FuncMap{
7880
"join": strings.Join,
79-
"languageList": func(langs map[string]int) string {
80-
parts := make([]string, 0, len(langs))
81-
for lang, count := range langs {
82-
parts = append(parts, fmt.Sprintf("%s (%d)", lang, count))
83-
}
84-
return strings.Join(parts, ", ")
81+
"languageList": func(langs []string) string {
82+
return strings.Join(langs, ", ")
8583
},
8684
}
8785

@@ -104,22 +102,32 @@ func Render(graph *api.ProjectGraph, projectName string, opts RenderOptions) (st
104102
}
105103

106104
// If over budget, truncate domains to fit
107-
return truncateToTokenBudget(graph, projectName, opts.MaxTokens)
105+
return truncateToTokenBudget(graph, projectName, opts.MaxTokens, graph.Stats.CircularDependencyCycles)
108106
}
109107

110108
// truncateToTokenBudget progressively drops lower-priority content to fit the token budget.
111109
func truncateToTokenBudget(
112110
graph *api.ProjectGraph,
113111
projectName string,
114112
maxTokens int,
113+
circularCycles int,
115114
) (string, int, error) {
116115

117116
// Build a minimal required header
118-
required := fmt.Sprintf(`# Uncompact Context — %s
119-
120-
**Language:** %s · **Files:** %d · **Functions:** %d`,
121-
projectName, graph.Language, graph.Stats.TotalFiles, graph.Stats.TotalFunctions,
122-
)
117+
var hdr strings.Builder
118+
hdr.WriteString(fmt.Sprintf("# Uncompact Context — %s\n\n", projectName))
119+
if circularCycles > 0 {
120+
label := "cycles"
121+
if circularCycles == 1 {
122+
label = "cycle"
123+
}
124+
hdr.WriteString(fmt.Sprintf("> ⚠️ %d circular dependency %s detected\n\n", circularCycles, label))
125+
}
126+
hdr.WriteString(fmt.Sprintf(
127+
"**Language:** %s · **Files:** %d · **Functions:** %d",
128+
graph.Language, graph.Stats.TotalFiles, graph.Stats.TotalFunctions,
129+
))
130+
required := hdr.String()
123131

124132
reqTokens := countTokens(required)
125133
if reqTokens > maxTokens {
@@ -161,6 +169,16 @@ func buildDomainSection(d api.Domain) string {
161169
if len(d.KeyFiles) > 0 {
162170
sb.WriteString(fmt.Sprintf("**Key files:** %s\n", strings.Join(d.KeyFiles, ", ")))
163171
}
172+
if len(d.Subdomains) > 0 {
173+
sb.WriteString("**Subdomains:**\n")
174+
for _, s := range d.Subdomains {
175+
if s.Description != "" {
176+
sb.WriteString(fmt.Sprintf("- %s: %s\n", s.Name, s.Description))
177+
} else {
178+
sb.WriteString(fmt.Sprintf("- %s\n", s.Name))
179+
}
180+
}
181+
}
164182
return sb.String()
165183
}
166184

0 commit comments

Comments
 (0)