-
Notifications
You must be signed in to change notification settings - Fork 10
feat: add --three-file flag to generate .calls/.deps/.impact shards #120
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
68d8fa9
56b6c3a
f33b7d8
3d1be34
fc10eba
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -102,11 +102,46 @@ Manages `.graph.*` sidecar files written next to each source file. Agents read t | |
|
|
||
| | Command | Description | | ||
| |---|---| | ||
| | `analyze [path]` | Upload repo, run full analysis, write `.graph.*` files (use `--no-files` to skip) | | ||
| | `analyze [path]` | Upload repo, run full analysis, write graph files (use `--three-file` for best results, `--no-shards` to skip) | | ||
| | `skill` | Print agent awareness prompt — pipe to `CLAUDE.md` or `AGENTS.md` | | ||
| | `watch [path]` | Generate graph files on startup, then keep them updated incrementally | | ||
| | `clean [path]` | Remove all `.graph.*` files from the repository | | ||
| | `hook` | Claude Code `PostToolUse` hook — forward file-change events to the `watch` daemon | | ||
|
|
||
| ### Three-file shard format (recommended) | ||
|
|
||
| For best results, use the `--three-file` flag to generate separate `.calls`, `.deps`, and `.impact` files instead of a single `.graph` file: | ||
|
|
||
| ```bash | ||
| supermodel analyze --three-file | ||
| ``` | ||
|
|
||
| This produces three files per source file: | ||
|
|
||
| ``` | ||
| src/cache.go → src/cache.calls.go # who calls what, with file:line | ||
| → src/cache.deps.go # imports and imported-by | ||
| → src/cache.impact.go # risk level, domains, blast radius | ||
|
Comment on lines
+122
to
+124
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix the inconsistent file extension notation. Your example at lines 122-124 shows specific filenames like If the actual generated files are ✏️ Suggested fix for consistencyOption 1: If files always include the source language extension, update the manual text to match the example: -Files ending in .calls.* contain function call relationships.
-Files ending in .deps.* contain dependency relationships.
-Files ending in .impact.* contain blast radius data.
+Files ending in .calls.go (or .calls.py, etc.) contain function call relationships.
+Files ending in .deps.go (or .deps.py, etc.) contain dependency relationships.
+Files ending in .impact.go (or .impact.py, etc.) contain blast radius data.Option 2: If you want to use wildcards for brevity, update the example to match: -src/cache.go → src/cache.calls.go # who calls what, with file:line
- → src/cache.deps.go # imports and imported-by
- → src/cache.impact.go # risk level, domains, blast radius
+src/cache.go → src/cache.calls.* # who calls what, with file:line
+ → src/cache.deps.* # imports and imported-by
+ → src/cache.impact.* # risk level, domains, blast radiusAlso applies to: 139-141 🤖 Prompt for AI Agents |
||
| ``` | ||
|
Comment on lines
+121
to
+125
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add language specifiers to code blocks. The markdown linter is flagging these code blocks because they're missing language identifiers. Add 🔧 Quick fixFor the file listing at line 121: -```
+```text
src/cache.go → src/cache.calls.go # who calls what, with file:lineFor the manual instruction at line 137: -```
+```text
This repository has Supermodel graph shard files next to source files.Also applies to: 137-143 🧰 Tools🪛 markdownlint-cli2 (0.22.0)[warning] 121-121: Fenced code blocks should have a language specified (MD040, fenced-code-language) 🤖 Prompt for AI Agents |
||
|
|
||
| The three-file format is **68% faster** in benchmarks because grep hits are more targeted — searching for a function name hits only the `.calls` file with caller/callee data, not a combined blob. | ||
|
|
||
| **Tell your agent about the files** by adding this to `CLAUDE.md` or `AGENTS.md`: | ||
|
|
||
| ```bash | ||
| supermodel skill >> CLAUDE.md | ||
| ``` | ||
|
|
||
| Or manually add: | ||
|
|
||
| ``` | ||
| This repository has Supermodel graph shard files next to source files. | ||
| Files ending in .calls.* contain function call relationships. | ||
| Files ending in .deps.* contain dependency relationships. | ||
| Files ending in .impact.* contain blast radius data. | ||
| Read these files to understand relationships between modules before making changes. | ||
| ``` | ||
|
|
||
| ### On-demand analysis | ||
|
|
||
| | Command | Description | | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -26,6 +26,13 @@ func ShardFilename(sourcePath string) string { | |
| return stem + ".graph" + ext | ||
| } | ||
|
|
||
| // ThreeFileShardNames generates the .calls, .deps, .impact shard paths. | ||
| func ThreeFileShardNames(sourcePath string) (calls, deps, impact string) { | ||
| ext := filepath.Ext(sourcePath) | ||
| stem := strings.TrimSuffix(sourcePath, ext) | ||
| return stem + ".calls" + ext, stem + ".deps" + ext, stem + ".impact" + ext | ||
| } | ||
|
|
||
| // Header returns the @generated header line. | ||
| func Header(prefix string) string { | ||
| return prefix + " @generated supermodel-shard — do not edit\n" | ||
|
|
@@ -236,20 +243,51 @@ func WriteShard(repoDir, shardPath, content string, dryRun bool) error { | |
| return nil | ||
| } | ||
|
|
||
| // safeRemove removes a file only if it resolves inside repoDir (traversal guard). | ||
| func safeRemove(repoDir, relPath string) { | ||
| full, err := filepath.Abs(filepath.Join(repoDir, relPath)) | ||
| if err != nil { | ||
| return | ||
| } | ||
| repoAbs, err := filepath.Abs(repoDir) | ||
| if err != nil { | ||
| return | ||
| } | ||
| if !strings.HasPrefix(full, repoAbs+string(filepath.Separator)) && full != repoAbs { | ||
| return | ||
| } | ||
| _ = os.Remove(full) | ||
| } | ||
|
|
||
| // removeStaleThreeFile removes .calls/.deps/.impact files for a source file. | ||
| func removeStaleThreeFile(repoDir, srcFile string) { | ||
| c, d, i := ThreeFileShardNames(srcFile) | ||
| for _, p := range []string{c, d, i} { | ||
| safeRemove(repoDir, p) | ||
| } | ||
| } | ||
|
|
||
| // removeStaleGraph removes the single .graph file for a source file. | ||
| func removeStaleGraph(repoDir, srcFile string) { | ||
| safeRemove(repoDir, ShardFilename(srcFile)) | ||
| } | ||
|
|
||
| // RenderAll generates and writes .graph shards for the given source files. | ||
| // Returns the count of shards written. | ||
| func RenderAll(repoDir string, cache *Cache, files []string, dryRun bool) (int, error) { | ||
| sort.Strings(files) | ||
| written := 0 | ||
|
|
||
| for _, srcFile := range files { | ||
| // Clean up stale three-file shards from a previous --three-file run. | ||
| removeStaleThreeFile(repoDir, srcFile) | ||
|
|
||
| ext := filepath.Ext(srcFile) | ||
| prefix := CommentPrefix(ext) | ||
| header := Header(prefix) | ||
|
|
||
| content := RenderGraph(srcFile, cache, prefix) | ||
| if content == "" { | ||
| // Remove any stale shard left from a previous run. | ||
| full := filepath.Join(repoDir, ShardFilename(srcFile)) | ||
| _ = os.Remove(full) | ||
| continue | ||
|
|
@@ -273,6 +311,55 @@ func RenderAll(repoDir string, cache *Cache, files []string, dryRun bool) (int, | |
| return written, nil | ||
| } | ||
|
|
||
| // RenderAllThreeFile generates .calls, .deps, and .impact files per source file. | ||
| func RenderAllThreeFile(repoDir string, cache *Cache, files []string, dryRun bool) (int, error) { | ||
| sort.Strings(files) | ||
| written := 0 | ||
|
|
||
| for _, srcFile := range files { | ||
| // Clean up stale single .graph file from a previous non-three-file run. | ||
| removeStaleGraph(repoDir, srcFile) | ||
|
|
||
| ext := filepath.Ext(srcFile) | ||
| prefix := CommentPrefix(ext) | ||
| header := Header(prefix) | ||
| goPrefix := "" | ||
| if ext == ".go" { | ||
| goPrefix = "//go:build ignore\n\npackage ignore\n" | ||
| } | ||
|
|
||
| callsPath, depsPath, impactPath := ThreeFileShardNames(srcFile) | ||
|
|
||
| deps := renderDepsSection(srcFile, cache, prefix) | ||
| calls := renderCallsSection(srcFile, cache, prefix) | ||
| impact := renderImpactSection(srcFile, cache, prefix) | ||
|
|
||
| for _, item := range []struct { | ||
| path string | ||
| content string | ||
| }{ | ||
| {depsPath, deps}, | ||
| {callsPath, calls}, | ||
| {impactPath, impact}, | ||
| } { | ||
| if item.content == "" { | ||
| safeRemove(repoDir, item.path) | ||
| continue | ||
|
Comment on lines
+345
to
+347
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Protect stale-file deletes (dry-run + path safety). This branch deletes files even in dry-run, and it bypasses the path traversal guard used by Safer pattern for _, item := range []struct {
path string
content string
}{
{depsPath, deps},
{callsPath, calls},
{impactPath, impact},
} {
if item.content == "" {
- full := filepath.Join(repoDir, item.path)
- _ = os.Remove(full)
+ if err := RemoveShard(repoDir, item.path, dryRun); err != nil {
+ return written, err
+ }
continue
}func RemoveShard(repoDir, shardPath string, dryRun bool) error {
full, err := filepath.Abs(filepath.Join(repoDir, shardPath))
if err != nil {
return err
}
repoAbs, err := filepath.Abs(repoDir)
if err != nil {
return err
}
if !strings.HasPrefix(full, repoAbs+string(filepath.Separator)) && full != repoAbs {
return fmt.Errorf("path traversal blocked: %s", shardPath)
}
if dryRun {
fmt.Printf(" [dry-run] would remove %s\n", full)
return nil
}
if err := os.Remove(full); err != nil && !os.IsNotExist(err) {
return err
}
return nil
}🤖 Prompt for AI Agents |
||
| } | ||
| fullContent := goPrefix + header + item.content + "\n" | ||
| if err := WriteShard(repoDir, item.path, fullContent, dryRun); err != nil { | ||
| if strings.Contains(err.Error(), "path traversal") { | ||
| continue | ||
| } | ||
| return written, err | ||
| } | ||
| written++ | ||
| } | ||
| } | ||
|
|
||
| return written, nil | ||
| } | ||
|
|
||
| func formatLoc(file string, line int) string { | ||
| if file != "" && line > 0 { | ||
| return fmt.Sprintf("%s:%d", file, line) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: supermodeltools/cli
Length of output: 45
🏁 Script executed:
Repository: supermodeltools/cli
Length of output: 2139
🏁 Script executed:
Repository: supermodeltools/cli
Length of output: 4084
🏁 Script executed:
Repository: supermodeltools/cli
Length of output: 2369
Remove the
skillcommand from README or implement it.The README documents a
skillcommand (lines 106, 132) with usage examples, but it doesn't exist in the codebase. There's nocmd/skill.gofile like the other documented commands (analyze, clean, hook, etc. all have their implementation files).Either remove these lines from the README, or implement the command. Right now users following the docs will hit a "command not found" error.
🤖 Prompt for AI Agents