Skip to content

Show graph stats summary after watch generates initial files#55

Merged
greynewell merged 3 commits intomainfrom
feature/watch-startup-summary
Apr 7, 2026
Merged

Show graph stats summary after watch generates initial files#55
greynewell merged 3 commits intomainfrom
feature/watch-startup-summary

Conversation

@greynewell
Copy link
Copy Markdown
Contributor

@greynewell greynewell commented Apr 7, 2026

Closes #51

What changed

When supermodel watch completes its initial generate — or loads from an existing cache — it now prints a human-readable summary:

  ✓  847 files · 12,340 functions · 4,521 relationships  (fetched)

Incremental updates (triggered by the Claude Code hook) print a shorter line:

  ✓  Updated — 847 files · 12,340 functions · 4,521 relationships

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

  • GraphStats struct + computeStats() in internal/files/graph.go
  • OnReady(GraphStats) and OnUpdate(GraphStats) callbacks on DaemonConfig (replaces the old no-arg OnReady func())
  • loadedCache bool field on Daemon to distinguish live API fetch from cache hit
  • Styled stdout callbacks wired in files.Watch() in handler.go

Test plan

  • supermodel watch with no existing cache → shows (fetched) summary after analysis completes
  • supermodel watch with existing cache → shows (cached) summary immediately
  • Edit a source file while watch is running → shows Updated — line after debounce

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Watch status now shows counts of analyzed source files, functions, and relationships.
    • Readiness message indicates whether data came from cache or a fresh analysis.
    • Reports include an "uncalled"/dead-function count when applicable.
    • Incremental updates emit refreshed "Updated" summary statistics after changes.

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>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 7, 2026

Walkthrough

Daemon now computes GraphStats (SourceFiles, Functions, Relationships, DeadFunctionCount, FromCache) after initial load and on incremental updates, passes those stats to new callbacks OnReady(GraphStats) and OnUpdate(GraphStats), and the handler prints formatted summaries for initial and update events.

Changes

Cohort / File(s) Summary
Graph Statistics Foundation
internal/files/graph.go
Added exported GraphStats (SourceFiles, Functions, Relationships, DeadFunctionCount, FromCache) and computeStats(ir *api.SidecarIR, c *Cache) GraphStats. Counts file/function nodes, relationships, and marks functions with no callers as dead; FromCache is untouched by compute function.
Daemon Lifecycle Callbacks
internal/files/daemon.go
DaemonConfig.OnReady signature changed to func(GraphStats); added OnUpdate func(GraphStats) and internal loadedCache bool. After loadOrGenerate, daemon computes stats, sets stats.FromCache = loadedCache, writes ready status using stats.SourceFiles, and calls OnReady(stats). During incremental updates, it recomputes full GraphStats, updates status using updateStats.SourceFiles, saves cache under mutex, and calls OnUpdate(updateStats) if set.
User-Facing Output
internal/files/handler.go
Added ANSI styling constants and wired Watch to assign OnReady and OnUpdate callbacks. OnReady prints an initial "Mapped" summary including cached vs fetched and shows dead/uncalled counts when >0; OnUpdate prints a shorter "Updated — X files mapped" summary (no cached/fetched label).

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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

A watcher wakes, it counts and shows,
Files and functions in tidy rows.
Cached or fresh, the totals hum—
“Mapped!” it cries, the job is done. ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: displaying a graph stats summary when watch generates initial files.
Description check ✅ Passed The description is complete, covering What/Why/Implementation and includes a detailed test plan with manual testing scenarios.
Linked Issues check ✅ Passed The PR implementation addresses all core requirements from issue #51: computes and displays source file/function/relationship counts with cache source indicator and shows updated summaries on incremental changes.
Out of Scope Changes check ✅ Passed All changes are directly scoped to the linked issue: new GraphStats struct, callbacks for lifecycle events, and styled output formatting—no extraneous modifications.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/watch-startup-summary

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 46ca8f6 and fe97ffa.

📒 Files selected for processing (3)
  • internal/files/daemon.go
  • internal/files/graph.go
  • internal/files/handler.go

return d.fullGenerate(ctx)
}


Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +263 to +264
d.writeStatus(fmt.Sprintf("ready — %s — %d nodes",
time.Now().Format(time.RFC3339), nodeCount))
time.Now().Format(time.RFC3339), updateStats.SourceFiles))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
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.

Comment on lines +48 to +54
// 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
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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).

Comment on lines +56 to +71
// 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
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Suggested change
// 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>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
internal/files/daemon.go (1)

262-263: ⚠️ Potential issue | 🟡 Minor

Status label/value mismatch is still present.

At Line 263, the value is updateStats.SourceFiles but the label says nodes. 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: Reset loadedCache on non-cache startup path to avoid stale state on daemon reuse.

Line 159 sets loadedCache = true on cache hit, but there’s no explicit reset before fallback/full-generate. If the same Daemon instance is reused, FromCache can 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

📥 Commits

Reviewing files that changed from the base of the PR and between fe97ffa and 6fa1193.

📒 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>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
internal/files/daemon.go (1)

82-83: ⚠️ Potential issue | 🟡 Minor

Startup 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. Use stats.SourceFiles here.

💡 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

📥 Commits

Reviewing files that changed from the base of the PR and between 6fa1193 and 570852a.

📒 Files selected for processing (3)
  • internal/files/daemon.go
  • internal/files/graph.go
  • internal/files/handler.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • internal/files/handler.go
  • internal/files/graph.go

@greynewell greynewell merged commit 1ca605e into main Apr 7, 2026
7 checks passed
@greynewell greynewell deleted the feature/watch-startup-summary branch April 7, 2026 21:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Show graph stats summary when watch generates initial files

1 participant