Skip to content

fix: eliminate double API call in analyze when file mode is on#63

Merged
jonathanpopham merged 1 commit intosupermodeltools:mainfrom
jonathanpopham:fix/double-api-call
Apr 8, 2026
Merged

fix: eliminate double API call in analyze when file mode is on#63
jonathanpopham merged 1 commit intosupermodeltools:mainfrom
jonathanpopham:fix/double-api-call

Conversation

@jonathanpopham
Copy link
Copy Markdown
Contributor

@jonathanpopham jonathanpopham commented Apr 7, 2026

Problem

supermodel analyze makes two API calls to the same /v1/graphs/supermodel endpoint:

  1. analyze.Run() — uploads zip, gets graph, caches to ~/.supermodel/cache/
  2. files.Generate() — uploads zip again, gets sidecar IR, caches to .supermodel/graph.json

Same zip, same endpoint, two uploads. Users wait through two "Uploading and analyzing" phases.

Fix

When file mode is on (default), skip analyze.Run() entirely. Let files.Generate() handle the full pipeline in a single API call. When --no-files is set, use the original analyze.Run() path.

The --force flag is passed through to files.Generate() so it bypasses the sidecar cache when requested.

1 file changed, 5 lines.

Test plan

  • go test ./... — all tests pass
  • go vet ./... — clean
  • supermodel analyze --force — one "Uploading" spinner, writes sidecars, prints summary
  • supermodel analyze --no-files --force — one "Uploading" spinner, prints summary table, no sidecars
  • supermodel analyze (cached) — "Using cached graph", renders from cache, no API call

Summary by CodeRabbit

  • Bug Fixes

    • Fixed inconsistent --force flag behavior when file output is enabled. The flag now applies consistently across analysis and file generation operations.
  • Refactor

    • Optimized command execution flow to conditionally process file generation or analysis based on configuration settings.

When file mode is enabled (default), `supermodel analyze` was making
two separate API calls to the same endpoint: first via analyze.Run()
(caching to ~/.supermodel/cache/), then via files.Generate() (caching
to .supermodel/graph.json). Both uploaded the same zip to
/v1/graphs/supermodel.

Fix: when file mode is on, skip analyze.Run() and let files.Generate()
handle the entire pipeline (upload + cache + sidecars) in a single API
call. When --no-files is set, use the original analyze.Run() path.

The --force flag is passed through to files.Generate() so it bypasses
the sidecar cache when requested.
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 7, 2026

Walkthrough

The command control flow in cmd/analyze.go was reordered to conditionally execute files.Generate() when file output is enabled, otherwise running analyze.Run(). Additionally, the --force flag is now explicitly passed to the file generation options, ensuring consistent behavior across both execution paths.

Changes

Cohort / File(s) Summary
Control Flow Refactoring
cmd/analyze.go
Reordered conditional logic so file generation runs when enabled, otherwise analysis runs. The --force option is now explicitly passed to files.GenerateOptions instead of using defaults.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested reviewers

  • greynewell

Poem

🔄 Control flows reshape, like morning routines anew
Files generate first, when conditions ring true
The --force flag travels along for the ride
Simpler paths, cleaner logic—no early escape to hide
Five lines traded, but clarity multiplied ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main fix: eliminating a double API call that occurs in analyze when file mode is enabled.
Description check ✅ Passed The description follows the template with all required sections filled out: Problem explains the issue clearly, Fix describes the solution, and Test plan documents comprehensive testing including go tests, vet, manual testing with various flags, and cached behavior.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cmd/analyze.go`:
- Around line 39-45: When cfg.FilesEnabled() && !noFiles causes an early return
to files.Generate, it skips analyze.Run's responsibilities (printing
summary/JSON and writing the content-hash cache). Update the branch so that
after calling files.Generate(cmd.Context(), cfg, dir,
files.GenerateOptions{Force: opts.Force}) you also invoke
analyze.Run(cmd.Context(), cfg, dir, opts) (or otherwise call the functions in
analyze.Run that emit JSON/summary and persist the cache) and ensure the command
output/exit behavior still reflects analyze.Run's result; keep references to
cfg.FilesEnabled, noFiles, files.Generate, files.GenerateOptions, and
analyze.Run to locate and implement the change.
- Around line 39-43: The current branch returns files.Generate(...) early which
trusts .supermodel/graph.json and can serve stale results because files.Generate
only checks file existence when --force is false; instead compute the same repo
fingerprint/content hash and cache validation used by analyze.Run (see
internal/analyze/handler.go fingerprinting logic) before calling files.Generate,
and if the fingerprint indicates the cache is valid let files.Generate proceed,
otherwise invalidate or regenerate the graph (or pass a Force-equivalent) so
files.Generate does not reuse stale .supermodel/graph.json; update
cmd/analyze.go to invoke the same fingerprinting/check routine (or extract it
into a helper used by both analyze.Run and files.Generate) so files.Generate
only runs when content-based validation passes.
🪄 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: 00fc9259-ba3c-4927-ae89-40abeb83195c

📥 Commits

Reviewing files that changed from the base of the PR and between 0e76d36 and 1cdb397.

📒 Files selected for processing (1)
  • cmd/analyze.go

Comment on lines 39 to +45
if cfg.FilesEnabled() && !noFiles {
return files.Generate(cmd.Context(), cfg, dir, files.GenerateOptions{})
// File mode: Generate handles the full pipeline (API call +
// cache + sidecars) in a single upload. Running analyze.Run
// first would duplicate the API call.
return files.Generate(cmd.Context(), cfg, dir, files.GenerateOptions{Force: opts.Force})
}
return nil
return analyze.Run(cmd.Context(), cfg, dir, opts)
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

Skipping analyze.Run also drops two analyze behaviors users already have.

analyze.Run is the thing that prints the requested summary/JSON and writes the content-hash cache advertised in this command’s help (internal/analyze/handler.go:21-92). files.Generate only renders sidecars and persists .supermodel/graph.json (internal/files/handler.go:82-132). So in the default mode, supermodel analyze -o json no longer behaves like JSON output, and this path never warms the shared cache for follow-up analysis commands.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/analyze.go` around lines 39 - 45, When cfg.FilesEnabled() && !noFiles
causes an early return to files.Generate, it skips analyze.Run's
responsibilities (printing summary/JSON and writing the content-hash cache).
Update the branch so that after calling files.Generate(cmd.Context(), cfg, dir,
files.GenerateOptions{Force: opts.Force}) you also invoke
analyze.Run(cmd.Context(), cfg, dir, opts) (or otherwise call the functions in
analyze.Run that emit JSON/summary and persist the cache) and ensure the command
output/exit behavior still reflects analyze.Run's result; keep references to
cfg.FilesEnabled, noFiles, files.Generate, files.GenerateOptions, and
analyze.Run to locate and implement the change.

Comment on lines 39 to +43
if cfg.FilesEnabled() && !noFiles {
return files.Generate(cmd.Context(), cfg, dir, files.GenerateOptions{})
// File mode: Generate handles the full pipeline (API call +
// cache + sidecars) in a single upload. Running analyze.Run
// first would duplicate the API call.
return files.Generate(cmd.Context(), cfg, dir, files.GenerateOptions{Force: opts.Force})
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 | 🔴 Critical

This makes the default analyze path serve stale results after the first run.

files.Generate only checks whether .supermodel/graph.json exists when --force is false (internal/files/handler.go:50-81). It never fingerprints or hashes the current repo the way analyze.Run does (internal/analyze/handler.go:33-69). Easy repro: run analyze once, edit any source file, run it again, and this branch will still reuse the old sidecar cache instead of analyzing the new repo. To keep the single-upload win, this path needs the same content-based cache validation before trusting .supermodel/graph.json.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/analyze.go` around lines 39 - 43, The current branch returns
files.Generate(...) early which trusts .supermodel/graph.json and can serve
stale results because files.Generate only checks file existence when --force is
false; instead compute the same repo fingerprint/content hash and cache
validation used by analyze.Run (see internal/analyze/handler.go fingerprinting
logic) before calling files.Generate, and if the fingerprint indicates the cache
is valid let files.Generate proceed, otherwise invalidate or regenerate the
graph (or pass a Force-equivalent) so files.Generate does not reuse stale
.supermodel/graph.json; update cmd/analyze.go to invoke the same
fingerprinting/check routine (or extract it into a helper used by both
analyze.Run and files.Generate) so files.Generate only runs when content-based
validation passes.

@jonathanpopham
Copy link
Copy Markdown
Contributor Author

Proof of double API call

On main (before this PR), supermodel analyze --force from clean state:

Two distinct success lines (two API calls):

✓ Analysis complete (repoId: )       ← analyze.Run() → POST /v1/graphs/supermodel
✓ Wrote 119 sidecars for 125 files   ← files.Generate() → POST /v1/graphs/supermodel (again)

Root cause: analyze.Run() caches to ~/.supermodel/cache/{hash}.json. files.Generate() checks .supermodel/graph.json. Different locations, so the second call never finds the first call's result:

$ rm -rf .supermodel ~/.supermodel/cache
$ supermodel analyze --force --no-files   # just analyze.Run
$ ls ~/.supermodel/cache/*.json | wc -l
2                                         # fingerprint key + zip hash key
$ ls .supermodel/graph.json
No such file or directory                 # files.Generate cache doesn't exist

When files.Generate() runs next, it checks .supermodel/graph.json, doesn't find it, and makes a second API call.

After this PR: file mode skips analyze.Run() entirely. Single API call, single spinner, single success line.

@jonathanpopham
Copy link
Copy Markdown
Contributor Author

Logs proving fix works

Before (main) — two API calls

$ rm -rf .supermodel ~/.supermodel/cache
$ supermodel analyze --force

✓ Analysis complete (repoId: )          ← analyze.Run() API call #1
[spinner: Uploading and analyzing...]   ← files.Generate() API call #2
✓ Wrote 119 sidecars for 125 files

Proof the cache miss causes the second call:

$ supermodel analyze --force --no-files  # just analyze.Run
$ ls ~/.supermodel/cache/*.json | wc -l
2                                        # writes here
$ ls .supermodel/graph.json
No such file or directory                # files.Generate looks HERE — miss

After (this PR) — one API call

$ rm -rf .supermodel ~/.supermodel/cache
$ supermodel analyze --force

✓ Wrote 119 sidecars for 125 source files (1852 nodes, 4086 relationships)

One spinner, one success line, one API call.

All paths verified

Scenario API calls Sidecars Output
analyze --force 1 119 ✓ Wrote 119 sidecars...
analyze (cached) 0 119 ✓ Using cached graph (1852 nodes)
analyze --no-files --force 1 0 ✓ Analysis complete + summary table

@jonathanpopham
Copy link
Copy Markdown
Contributor Author

Full impact analysis

What analyze.Run() does that files.Generate() doesn't

analyze.Run()GetGraph() writes to ~/.supermodel/cache/ (fingerprint key + zip hash key). This is a shared graph cache that other commands check before making their own API call.

files.Generate() writes to .supermodel/graph.json (sidecar-specific cache). It does not write to ~/.supermodel/cache/.

Who reads ~/.supermodel/cache/?

Commands that call cache.Get() and benefit from a pre-warmed cache:

Command Uses cache.Get? Impact of skipping analyze.Run()
analyze (itself) Yes N/A — we're changing this command
find Yes First run after analyze will make its own API call instead of cache hit
focus Yes Same — first run makes its own API call
mcp Yes Same — first tool use makes its own API call
graph Yes (via GetGraph) Same

Who doesn't care?

Commands that never check cache.Get() — they always create their own zip and hit their own API endpoint regardless:

Command Endpoint Affected?
dead-code /v1/analysis/dead-code No — always uploads its own zip
blast-radius /v1/analysis/impact No — always uploads its own zip
audit Both endpoints No — always uploads
docs (archdocs) /v1/graphs/supermodel No — always uploads
restore /v1/graphs/supermodel No — always uploads
share /v1/share No — different endpoint

Do sidecars still work?

Yes, completely unaffected. files.Generate() calls AnalyzeSidecars → returns *api.SidecarIR with full nodes/labels/properties → writes .supermodel/graph.json → renders sidecars. This is the exact same code path as before, just without analyze.Run() running first.

The watch daemon reads from .supermodel/graph.json, not ~/.supermodel/cache/. Incremental updates, sidecar rendering, domain preservation — all use the sidecar cache. None of them depend on the analyze cache.

What's actually lost

If a user runs supermodel analyze then immediately runs supermodel find <symbol>, the find command used to get a cache hit from the graph that analyze pre-warmed. Now it will make its own API call (~30-60s) because the fingerprint cache at ~/.supermodel/cache/ wasn't populated.

This only affects find, focus, graph, and mcp — and only on the first run after analyze. These commands all populate the cache themselves, so subsequent runs are cached either way.

Is this a regression?

Marginal. The pre-warming was a side effect of analyze.Run(), not an intentional design. Before file mode existed (pre-PR #45), analyze only called analyze.Run(). The cache pre-warming was always there. PR #45 added files.Generate() as a second call, doubling the API cost. This PR removes the first call to eliminate the double API cost. The pre-warming was collateral.

The proper fix (future work) would be to have files.Generate() also write to ~/.supermodel/cache/ so both caches stay warm from a single API call. That requires converting SidecarIRGraph or having the cache accept both types.

@jonathanpopham
Copy link
Copy Markdown
Contributor Author

Supermodel impact analysis

$ supermodel blast-radius cmd/analyze.go
Risk: low  |  Direct: 1  |  Transitive: 1  |  Files: 1
→ Affects 1 entry point: main.go

$ supermodel blast-radius internal/files/handler.go
Risk: critical  |  Direct: 21  |  Transitive: 23  |  Files: 7
→ High fan-in (21 direct callers)
→ Crosses 2 domain boundaries
→ Affected: setup/wizard.go (9), cmd/root.go (5), cmd/analyze.go (3), cmd/clean.go (2), cmd/hook.go (2)

This PR only changes cmd/analyze.go (low risk, 1 dependent). It changes which function gets called — not the functions themselves. internal/files/handler.go (critical risk, 21 callers) is untouched.

@jonathanpopham jonathanpopham merged commit 25f1038 into supermodeltools:main Apr 8, 2026
7 checks passed
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.

1 participant