Skip to content

Commit 47c8f36

Browse files
greynewellclaude
andauthored
Add fingerprint caching to archdocs and audit commands (#70)
Both commands were re-uploading the repo on every invocation with no cache check. This adds the same fingerprint-based fast path used by `analyze`, `dead-code`, and `blast-radius`. - archdocs: cache raw API response by git fingerprint; skip upload on subsequent calls with an unchanged repo - audit: cache both the domain analysis and impact results by fingerprint; also deduplicate config.Load() and share the context across both API calls (no behavioural change — cosmetic cleanup) Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent e6e4961 commit 47c8f36

2 files changed

Lines changed: 98 additions & 40 deletions

File tree

cmd/audit.go

Lines changed: 51 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111

1212
"github.com/supermodeltools/cli/internal/api"
1313
"github.com/supermodeltools/cli/internal/audit"
14+
"github.com/supermodeltools/cli/internal/build"
1415
"github.com/supermodeltools/cli/internal/cache"
1516
"github.com/supermodeltools/cli/internal/config"
1617
)
@@ -50,15 +51,29 @@ func runAudit(cmd *cobra.Command, dir string) error {
5051
return err
5152
}
5253

53-
ir, err := auditAnalyze(cmd, rootDir, projectName)
54+
cfg, err := config.Load()
55+
if err != nil {
56+
return err
57+
}
58+
if err := cfg.RequireAPIKey(); err != nil {
59+
return err
60+
}
61+
62+
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
63+
defer cancel()
64+
65+
// Fingerprint for caching — best-effort; empty string means no caching.
66+
fp, _ := cache.RepoFingerprint(rootDir)
67+
68+
ir, err := auditAnalyze(ctx, cmd, cfg, rootDir, projectName, fp)
5469
if err != nil {
5570
return err
5671
}
5772

5873
report := audit.Analyze(ir, projectName)
5974

6075
// Run impact analysis (global mode) to enrich the health report.
61-
impact, err := runImpactForAudit(cmd, rootDir)
76+
impact, err := runImpactForAudit(ctx, cmd, cfg, rootDir, fp)
6277
if err != nil {
6378
fmt.Fprintf(cmd.ErrOrStderr(), "Warning: impact analysis unavailable: %v\n", err)
6479
} else {
@@ -81,13 +96,13 @@ func resolveAuditDir(dir string) (rootDir, projectName string, err error) {
8196
return rootDir, projectName, nil
8297
}
8398

84-
func auditAnalyze(cmd *cobra.Command, rootDir, projectName string) (*api.SupermodelIR, error) {
85-
cfg, err := config.Load()
86-
if err != nil {
87-
return nil, err
88-
}
89-
if err := cfg.RequireAPIKey(); err != nil {
90-
return nil, err
99+
func auditAnalyze(ctx context.Context, cmd *cobra.Command, cfg *config.Config, rootDir, projectName, fp string) (*api.SupermodelIR, error) {
100+
if fp != "" {
101+
key := cache.AnalysisKey(fp, "audit-domains", build.Version)
102+
var cached api.SupermodelIR
103+
if hit, _ := cache.GetJSON(key, &cached); hit {
104+
return &cached, nil
105+
}
91106
}
92107

93108
fmt.Fprintln(cmd.ErrOrStderr(), "Creating repository archive…")
@@ -103,18 +118,27 @@ func auditAnalyze(cmd *cobra.Command, rootDir, projectName string) (*api.Supermo
103118
}
104119

105120
client := api.New(cfg)
106-
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
107-
defer cancel()
108-
109121
fmt.Fprintf(cmd.ErrOrStderr(), "Analyzing %s…\n", projectName)
110-
return client.AnalyzeDomains(ctx, zipPath, "audit-"+hash[:16])
122+
ir, err := client.AnalyzeDomains(ctx, zipPath, "audit-"+hash[:16])
123+
if err != nil {
124+
return nil, err
125+
}
126+
127+
if fp != "" {
128+
key := cache.AnalysisKey(fp, "audit-domains", build.Version)
129+
_ = cache.PutJSON(key, ir)
130+
}
131+
return ir, nil
111132
}
112133

113134
// runImpactForAudit runs global impact analysis to enrich the health report.
114-
func runImpactForAudit(cmd *cobra.Command, rootDir string) (*api.ImpactResult, error) {
115-
cfg, err := config.Load()
116-
if err != nil {
117-
return nil, err
135+
func runImpactForAudit(ctx context.Context, cmd *cobra.Command, cfg *config.Config, rootDir, fp string) (*api.ImpactResult, error) {
136+
if fp != "" {
137+
key := cache.AnalysisKey(fp, "impact", build.Version)
138+
var cached api.ImpactResult
139+
if hit, _ := cache.GetJSON(key, &cached); hit {
140+
return &cached, nil
141+
}
118142
}
119143

120144
zipPath, err := audit.CreateZip(rootDir)
@@ -129,9 +153,15 @@ func runImpactForAudit(cmd *cobra.Command, rootDir string) (*api.ImpactResult, e
129153
}
130154

131155
client := api.New(cfg)
132-
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
133-
defer cancel()
134-
135156
fmt.Fprintln(cmd.ErrOrStderr(), "Running impact analysis…")
136-
return client.Impact(ctx, zipPath, "audit-impact-"+hash[:16], "", "")
157+
result, err := client.Impact(ctx, zipPath, "audit-impact-"+hash[:16], "", "")
158+
if err != nil {
159+
return nil, err
160+
}
161+
162+
if fp != "" {
163+
key := cache.AnalysisKey(fp, "impact", build.Version)
164+
_ = cache.PutJSON(key, result)
165+
}
166+
return result, nil
137167
}

internal/archdocs/handler.go

Lines changed: 47 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,13 @@ import (
1313
"strconv"
1414
"strings"
1515

16+
"encoding/json"
17+
1618
"github.com/supermodeltools/cli/internal/api"
1719
"github.com/supermodeltools/cli/internal/archdocs/graph2md"
1820
pssgbuild "github.com/supermodeltools/cli/internal/archdocs/pssg/build"
1921
pssgconfig "github.com/supermodeltools/cli/internal/archdocs/pssg/config"
22+
"github.com/supermodeltools/cli/internal/build"
2023
"github.com/supermodeltools/cli/internal/cache"
2124
"github.com/supermodeltools/cli/internal/config"
2225
"github.com/supermodeltools/cli/internal/ui"
@@ -282,28 +285,11 @@ func Run(ctx context.Context, cfg *config.Config, dir string, opts Options) erro
282285
opts.MaxEntities = 12000
283286
}
284287

285-
ui.Step("Creating repository archive…")
286-
zipPath, err := createZip(absDir)
287-
if err != nil {
288-
return fmt.Errorf("create archive: %w", err)
289-
}
290-
defer os.Remove(zipPath)
291-
292-
// Use zip hash as idempotency key (matches existing CLI cache key style)
293-
hash, err := cache.HashFile(zipPath)
288+
rawResult, err := analyzeOrCachedRaw(ctx, cfg, absDir, opts.Force)
294289
if err != nil {
295-
return fmt.Errorf("hash archive: %w", err)
290+
return err
296291
}
297292

298-
client := api.New(cfg)
299-
spin := ui.Start("Uploading and analyzing repository…")
300-
rawResult, err := client.AnalyzeRaw(ctx, zipPath, "archdocs-"+hash[:16])
301-
spin.Stop()
302-
if err != nil {
303-
return fmt.Errorf("API analysis: %w", err)
304-
}
305-
ui.Success("Analysis complete")
306-
307293
// Write raw graph JSON to a temp file for graph2md
308294
tmpDir, err := os.MkdirTemp("", "supermodel-archdocs-*")
309295
if err != nil {
@@ -491,3 +477,45 @@ func countFiles(dir, ext string) int {
491477
})
492478
return count
493479
}
480+
481+
// analyzeOrCachedRaw returns the raw JSON result from a repository analysis,
482+
// hitting the fingerprint cache first to avoid re-uploading unchanged repos.
483+
func analyzeOrCachedRaw(ctx context.Context, cfg *config.Config, repoDir string, force bool) (json.RawMessage, error) {
484+
if !force {
485+
if fp, err := cache.RepoFingerprint(repoDir); err == nil {
486+
key := cache.AnalysisKey(fp, "archdocs", build.Version)
487+
var cached json.RawMessage
488+
if hit, _ := cache.GetJSON(key, &cached); hit {
489+
ui.Success("Using cached analysis")
490+
return cached, nil
491+
}
492+
}
493+
}
494+
495+
ui.Step("Creating repository archive…")
496+
zipPath, err := createZip(repoDir)
497+
if err != nil {
498+
return nil, fmt.Errorf("create archive: %w", err)
499+
}
500+
defer os.Remove(zipPath)
501+
502+
hash, err := cache.HashFile(zipPath)
503+
if err != nil {
504+
return nil, fmt.Errorf("hash archive: %w", err)
505+
}
506+
507+
client := api.New(cfg)
508+
spin := ui.Start("Uploading and analyzing repository…")
509+
raw, err := client.AnalyzeRaw(ctx, zipPath, "archdocs-"+hash[:16])
510+
spin.Stop()
511+
if err != nil {
512+
return nil, fmt.Errorf("API analysis: %w", err)
513+
}
514+
ui.Success("Analysis complete")
515+
516+
if fp, err := cache.RepoFingerprint(repoDir); err == nil {
517+
key := cache.AnalysisKey(fp, "archdocs", build.Version)
518+
_ = cache.PutJSON(key, raw)
519+
}
520+
return raw, nil
521+
}

0 commit comments

Comments
 (0)