Show graph stats summary after watch generates initial files#55
Show graph stats summary after watch generates initial files#55greynewell merged 3 commits intomainfrom
Conversation
When `supermodel watch` completes its initial generate (or loads from cache), it now prints a human-readable summary line: ✓ 847 files · 12,340 functions · 4,521 relationships (fetched) Incremental updates after hook notifications print a shorter update: ✓ Updated — 847 files · 12,340 functions · 4,521 relationships Implementation: - Add GraphStats struct and computeStats() to internal/files/graph.go - Add OnReady(GraphStats) and OnUpdate(GraphStats) callbacks to DaemonConfig - Track loadedCache bool on Daemon to distinguish API fetch vs cache hit - Wire styled stdout callbacks in files.Watch() Closes #51 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
WalkthroughDaemon now computes GraphStats (SourceFiles, Functions, Relationships, DeadFunctionCount, FromCache) after initial load and on incremental updates, passes those stats to new callbacks Changes
Sequence Diagram(s)sequenceDiagram
participant CLI as CLI (watch)
participant Daemon as Files Daemon
participant IR as SidecarIR
participant Cache as Local Cache
participant Handler as Handler (stdout)
CLI->>Daemon: start Run()
Daemon->>IR: loadOrGenerate()
IR-->>Daemon: SidecarIR (graph)
Daemon->>Cache: check/load cache
Cache-->>Daemon: cache present? (sets loadedCache)
Daemon->>Daemon: computeStats(ir, cache)
Note right of Daemon: GraphStats{SourceFiles,Functions,Relationships,DeadFunctionCount,FromCache}
Daemon->>Handler: OnReady(GraphStats)
Handler-->>CLI: print "✓ Mapped X files — Y functions (cached|fetched)"
alt incremental change detected
Daemon->>IR: incrementalUpdate()
IR-->>Daemon: updated SidecarIR
Daemon->>Daemon: computeStats(ir, cache)
Daemon->>Cache: save updated cache (mutex)
Daemon->>Handler: OnUpdate(GraphStats)
Handler-->>CLI: print "✓ Updated — X files mapped"
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@internal/files/daemon.go`:
- Line 177: The lint failure is due to formatting/import ordering in daemon.go;
run goimports (or gofmt + goimports) on that file to fix formatting and organize
imports, then re-run the linter; ensure the resulting changes update the package
declarations and any functions/types in daemon.go (e.g., the Daemon type or
package daemon top-level declarations) so the file compiles and the goimports
error at the reported location is resolved.
- Around line 263-264: The status string passed to d.writeStatus is
inconsistent: it uses updateStats.SourceFiles but the message says "nodes";
update the format string or the value so they match—either change the literal
"nodes" to "files" in the call to d.writeStatus or replace
updateStats.SourceFiles with the correct node count field (e.g.,
updateStats.Nodes) so the reported label matches the numeric value; locate the
call to d.writeStatus and the updateStats struct usage to make the fix.
In `@internal/files/graph.go`:
- Around line 48-54: GraphStats is missing health metrics for cycles and dead
functions; extend the GraphStats struct by adding fields to capture cycle and
dead-function visibility (e.g., CycleCount int, DeadFunctionCount int, and
optionally DeadFunctionNames []string or Cycles [][]string as appropriate for
your consumers), then update all places that construct/populate GraphStats
(functions that return or aggregate GraphStats) to compute and set these new
fields and ensure any JSON/serialization usage is updated to include them (look
for usages of the GraphStats type and builders that set
SourceFiles/Functions/Relationships/FromCache).
- Around line 56-71: computeStats currently counts nodes with the "File" label
directly (in the loop over ir.Graph.Nodes) which can include non-renderable
files; instead use the canonical source-file list from the cache by calling
Cache.SourceFiles() to populate GraphStats.SourceFiles (e.g., set s.SourceFiles
= len(c.SourceFiles())) and remove the manual "File" label increment; ensure you
still handle a nil Cache pointer if c can be nil before calling c.SourceFiles().
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: de80c171-e71d-4cc0-8d74-aa5704252fb4
📒 Files selected for processing (3)
internal/files/daemon.gointernal/files/graph.gointernal/files/handler.go
internal/files/daemon.go
Outdated
| return d.fullGenerate(ctx) | ||
| } | ||
|
|
||
|
|
There was a problem hiding this comment.
Lint is failing on formatting in this file.
Pipeline shows goimports failure at Line 177; please run goimports (or gofmt + import fix) on this file before merge.
🧰 Tools
🪛 GitHub Actions: Lint
[error] 177-177: golangci-lint (goimports): File is not properly formatted (goimports check failed).
🪛 GitHub Check: golangci-lint
[failure] 177-177:
File is not properly formatted (goimports)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/files/daemon.go` at line 177, The lint failure is due to
formatting/import ordering in daemon.go; run goimports (or gofmt + goimports) on
that file to fix formatting and organize imports, then re-run the linter; ensure
the resulting changes update the package declarations and any functions/types in
daemon.go (e.g., the Daemon type or package daemon top-level declarations) so
the file compiles and the goimports error at the reported location is resolved.
internal/files/daemon.go
Outdated
| d.writeStatus(fmt.Sprintf("ready — %s — %d nodes", | ||
| time.Now().Format(time.RFC3339), nodeCount)) | ||
| time.Now().Format(time.RFC3339), updateStats.SourceFiles)) |
There was a problem hiding this comment.
Status text/value mismatch: writes “nodes” but stores file count.
At Line 264, the %d argument is updateStats.SourceFiles, but the message says “nodes”. This makes status output inaccurate for downstream readers.
💡 Proposed fix
- d.writeStatus(fmt.Sprintf("ready — %s — %d nodes",
- time.Now().Format(time.RFC3339), updateStats.SourceFiles))
+ d.writeStatus(fmt.Sprintf("ready — %s — %d files",
+ time.Now().Format(time.RFC3339), updateStats.SourceFiles))📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| d.writeStatus(fmt.Sprintf("ready — %s — %d nodes", | |
| time.Now().Format(time.RFC3339), nodeCount)) | |
| time.Now().Format(time.RFC3339), updateStats.SourceFiles)) | |
| d.writeStatus(fmt.Sprintf("ready — %s — %d files", | |
| time.Now().Format(time.RFC3339), updateStats.SourceFiles)) |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/files/daemon.go` around lines 263 - 264, The status string passed to
d.writeStatus is inconsistent: it uses updateStats.SourceFiles but the message
says "nodes"; update the format string or the value so they match—either change
the literal "nodes" to "files" in the call to d.writeStatus or replace
updateStats.SourceFiles with the correct node count field (e.g.,
updateStats.Nodes) so the reported label matches the numeric value; locate the
call to d.writeStatus and the updateStats struct usage to make the fix.
| // GraphStats summarises what was mapped after a generate or incremental update. | ||
| type GraphStats struct { | ||
| SourceFiles int | ||
| Functions int | ||
| Relationships int | ||
| FromCache bool // true when data was loaded from a local cache | ||
| } |
There was a problem hiding this comment.
GraphStats is missing issue-requested health metrics.
The linked objective calls for cycle/dead-function visibility, but the new stats shape only includes files/functions/relationships. That leaves the feature partially delivered for the requested summary.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/files/graph.go` around lines 48 - 54, GraphStats is missing health
metrics for cycles and dead functions; extend the GraphStats struct by adding
fields to capture cycle and dead-function visibility (e.g., CycleCount int,
DeadFunctionCount int, and optionally DeadFunctionNames []string or Cycles
[][]string as appropriate for your consumers), then update all places that
construct/populate GraphStats (functions that return or aggregate GraphStats) to
compute and set these new fields and ensure any JSON/serialization usage is
updated to include them (look for usages of the GraphStats type and builders
that set SourceFiles/Functions/Relationships/FromCache).
| // computeStats derives a GraphStats from a SidecarIR and its built Cache. | ||
| func computeStats(ir *api.SidecarIR, c *Cache) GraphStats { | ||
| s := GraphStats{ | ||
| Relationships: len(ir.Graph.Relationships), | ||
| } | ||
| for _, n := range ir.Graph.Nodes { | ||
| switch { | ||
| case n.HasLabel("File"): | ||
| s.SourceFiles++ | ||
| case n.HasLabel("Function"): | ||
| s.Functions++ | ||
| } | ||
| } | ||
| _ = c // reserved for future per-file breakdown | ||
| return s | ||
| } |
There was a problem hiding this comment.
SourceFiles count can drift from actual “source files” shown elsewhere.
At Line 63-66, counting raw "File" labels from IR can include nodes that aren’t renderable source files, while the codebase already defines canonical source-file filtering via Cache.SourceFiles(). This can produce misleading watch summaries.
💡 Proposed fix
func computeStats(ir *api.SidecarIR, c *Cache) GraphStats {
s := GraphStats{
Relationships: len(ir.Graph.Relationships),
}
- for _, n := range ir.Graph.Nodes {
+ if c != nil {
+ s.SourceFiles = len(c.SourceFiles())
+ }
+ for _, n := range ir.Graph.Nodes {
switch {
- case n.HasLabel("File"):
- s.SourceFiles++
case n.HasLabel("Function"):
s.Functions++
}
}
- _ = c // reserved for future per-file breakdown
return s
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // computeStats derives a GraphStats from a SidecarIR and its built Cache. | |
| func computeStats(ir *api.SidecarIR, c *Cache) GraphStats { | |
| s := GraphStats{ | |
| Relationships: len(ir.Graph.Relationships), | |
| } | |
| for _, n := range ir.Graph.Nodes { | |
| switch { | |
| case n.HasLabel("File"): | |
| s.SourceFiles++ | |
| case n.HasLabel("Function"): | |
| s.Functions++ | |
| } | |
| } | |
| _ = c // reserved for future per-file breakdown | |
| return s | |
| } | |
| // computeStats derives a GraphStats from a SidecarIR and its built Cache. | |
| func computeStats(ir *api.SidecarIR, c *Cache) GraphStats { | |
| s := GraphStats{ | |
| Relationships: len(ir.Graph.Relationships), | |
| } | |
| if c != nil { | |
| s.SourceFiles = len(c.SourceFiles()) | |
| } | |
| for _, n := range ir.Graph.Nodes { | |
| switch { | |
| case n.HasLabel("Function"): | |
| s.Functions++ | |
| } | |
| } | |
| return s | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/files/graph.go` around lines 56 - 71, computeStats currently counts
nodes with the "File" label directly (in the loop over ir.Graph.Nodes) which can
include non-renderable files; instead use the canonical source-file list from
the cache by calling Cache.SourceFiles() to populate GraphStats.SourceFiles
(e.g., set s.SourceFiles = len(c.SourceFiles())) and remove the manual "File"
label increment; ensure you still handle a nil Cache pointer if c can be nil
before calling c.SourceFiles().
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
♻️ Duplicate comments (1)
internal/files/daemon.go (1)
262-263:⚠️ Potential issue | 🟡 MinorStatus label/value mismatch is still present.
At Line 263, the value is
updateStats.SourceFilesbut the label saysnodes. This makes status output misleading.Suggested fix
- d.writeStatus(fmt.Sprintf("ready — %s — %d nodes", + d.writeStatus(fmt.Sprintf("ready — %s — %d files", time.Now().Format(time.RFC3339), updateStats.SourceFiles))🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/files/daemon.go` around lines 262 - 263, The status message written by d.writeStatus uses the label "nodes" but injects updateStats.SourceFiles, causing a mismatch; update the fmt.Sprintf call in the d.writeStatus invocation so the value corresponds to the "nodes" label (use updateStats.Nodes) or change the label to "source files" to match updateStats.SourceFiles—locate the d.writeStatus(...) call in internal/files/daemon.go and replace the mismatched symbol accordingly.
🧹 Nitpick comments (1)
internal/files/daemon.go (1)
159-159: ResetloadedCacheon non-cache startup path to avoid stale state on daemon reuse.Line 159 sets
loadedCache = trueon cache hit, but there’s no explicit reset before fallback/full-generate. If the sameDaemoninstance is reused,FromCachecan be wrong on later runs.Suggested tweak
func (d *Daemon) loadOrGenerate(ctx context.Context) error { + d.mu.Lock() + d.loadedCache = false + d.mu.Unlock() + data, err := os.ReadFile(d.cfg.CacheFile) if err == nil { var ir api.SidecarIR🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/files/daemon.go` at line 159, The DAEMON leaves d.loadedCache set true on a cache hit but never resets it when the cache path isn't taken, causing stale FromCache state on subsequent reuse; update the same method where d.loadedCache is toggled (the code that sets d.loadedCache = true) to explicitly reset d.loadedCache = false on the non-cache/fallback path (or at the start of the run) so that FromCache reflects the current run, and ensure any branches that bypass cache generation (e.g., the fallback/full-generate branch) also clear d.loadedCache.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@internal/files/daemon.go`:
- Around line 262-263: The status message written by d.writeStatus uses the
label "nodes" but injects updateStats.SourceFiles, causing a mismatch; update
the fmt.Sprintf call in the d.writeStatus invocation so the value corresponds to
the "nodes" label (use updateStats.Nodes) or change the label to "source files"
to match updateStats.SourceFiles—locate the d.writeStatus(...) call in
internal/files/daemon.go and replace the mismatched symbol accordingly.
---
Nitpick comments:
In `@internal/files/daemon.go`:
- Line 159: The DAEMON leaves d.loadedCache set true on a cache hit but never
resets it when the cache path isn't taken, causing stale FromCache state on
subsequent reuse; update the same method where d.loadedCache is toggled (the
code that sets d.loadedCache = true) to explicitly reset d.loadedCache = false
on the non-cache/fallback path (or at the start of the run) so that FromCache
reflects the current run, and ensure any branches that bypass cache generation
(e.g., the fallback/full-generate branch) also clear d.loadedCache.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: bdd5c74a-214a-487e-afbb-e1a22fed2ef1
📒 Files selected for processing (1)
internal/files/daemon.go
- Add DeadFunctionCount to GraphStats (functions with no callers), computed via Cache.Callers in computeStats() - Show uncalled count in OnReady summary when non-zero: ✓ 847 files · 12,340 functions · 4,521 relationships · 23 uncalled - Fix writeStatus label: was "nodes", now "files" to match SourceFiles value Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
♻️ Duplicate comments (1)
internal/files/daemon.go (1)
82-83:⚠️ Potential issue | 🟡 MinorStartup status still reports node count while labeling it as files.
At Line 82-Line 83, the label says “files” but the value is
len(d.ir.Graph.Nodes). That can overcount badly in real repos. Usestats.SourceFileshere.💡 Minimal fix
- d.writeStatus(fmt.Sprintf("ready — %s — %d files", - time.Now().Format(time.RFC3339), len(d.ir.Graph.Nodes))) + d.writeStatus(fmt.Sprintf("ready — %s — %d files", + time.Now().Format(time.RFC3339), stats.SourceFiles))🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/files/daemon.go` around lines 82 - 83, The startup status message currently shows len(d.ir.Graph.Nodes) but labels it as "files" — replace that count with the actual source-file count (stats.SourceFiles) so the status reads the correct metric; update the d.writeStatus call to use d.ir.Stats.SourceFiles (or the appropriate stats.SourceFiles value) instead of len(d.ir.Graph.Nodes) while keeping the timestamp formatting and "files" label intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@internal/files/daemon.go`:
- Around line 82-83: The startup status message currently shows
len(d.ir.Graph.Nodes) but labels it as "files" — replace that count with the
actual source-file count (stats.SourceFiles) so the status reads the correct
metric; update the d.writeStatus call to use d.ir.Stats.SourceFiles (or the
appropriate stats.SourceFiles value) instead of len(d.ir.Graph.Nodes) while
keeping the timestamp formatting and "files" label intact.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 2d135453-126a-41c3-b13c-e3602ba7cbb8
📒 Files selected for processing (3)
internal/files/daemon.gointernal/files/graph.gointernal/files/handler.go
🚧 Files skipped from review as they are similar to previous changes (2)
- internal/files/handler.go
- internal/files/graph.go
Closes #51
What changed
When
supermodel watchcompletes its initial generate — or loads from an existing cache — it now prints a human-readable summary:Incremental updates (triggered by the Claude Code hook) print a shorter line:
Why
This is the "wow" moment — the user just installed the product and watch starts generating files, but previously it emitted nothing visible until something went wrong. The stats line is the first proof the product is working and gives users something screenshot-worthy.
Implementation
GraphStatsstruct +computeStats()ininternal/files/graph.goOnReady(GraphStats)andOnUpdate(GraphStats)callbacks onDaemonConfig(replaces the old no-argOnReady func())loadedCache boolfield onDaemonto distinguish live API fetch from cache hitfiles.Watch()inhandler.goTest plan
supermodel watchwith no existing cache → shows(fetched)summary after analysis completessupermodel watchwith existing cache → shows(cached)summary immediatelyUpdated —line after debounce🤖 Generated with Claude Code
Summary by CodeRabbit