# saber

Code analysis and project context for R.

saber ("to know" in Spanish, pronounced [sah-BEHR](https://www.youtube.com/watch?v=m3WBocsw9lw)) parses R source into structured symbol indices, traces function callers across projects, discovers dependency graphs, generates project briefings, and cracks open installed packages for introspection. Built for AI coding agents that need to understand R code without guessing.

## Install

```r
install.packages("saber")
# or to install the development version
remotes::install_github("cornball-ai/saber")
```

## What it does

**9 exported functions.**

| Function | What it does |
|---|---|
| `symbols()` | Parse R source files into function defs and calls via `getParseData()` |
| `blast_radius()` | Find every caller of a function, across projects |
| `find_downstream()` | Find all projects that depend on a given package |
| `projects()` | Discover R package projects and their metadata |
| `briefing()` | Generate a project context briefing (metadata, dependents, memory, git log) |
| `pkg_exports()` | List exported functions with argument signatures |
| `pkg_internals()` | List internal (non-exported) functions |
| `pkg_help()` | Pull help documentation as markdown |
| `default_exclude()` | Default directories to skip when scanning |

## Examples

Index all function definitions and calls in a project:

```r
syms <- saber::symbols("~/myproject")
syms$defs  # data.frame: name, file, line, exported
syms$calls # data.frame: caller, callee, file, line
```

Find who calls a function (and where the damage lands if you change it):

```r
saber::blast_radius("my_function", project = "~/myproject")
#>   caller      project      file         line
#>   do_thing    myproject    main.R         42
#>   run_batch   downstream   pipeline.R     17
```

Discover projects and their dependencies:

```r
saber::projects()
#>   package   title                  version  path            depends  imports
#>   saber     Code Analysis for R    0.2.0    /home/troy/saber        ...

saber::find_downstream("jsonlite")
#>   [1] "chatterbox" "cornfab" "diffuseR" "llamaR" "llm.api"
#>   [6] "safetensors" "stt.api" "torch" "tts.api" "tuber" "whisper"
```

Generate a project briefing for an AI agent:

```r
saber::briefing("saber")
#> # Briefing: saber
#> _Generated 2026-03-25 00:30_
#>
#> ## Package
#> - **Name**: saber
#> - **Title**: Code Analysis and Project Context for R
#> - **Version**: 0.2.0
#>
#> ## Recent commits
#> - 7983478 Add r-ci GitHub Actions workflow
#> - ...
```

Inspect any installed package:

```r
saber::pkg_exports("saber")
saber::pkg_help("symbols", "saber")
```

## How it works

`symbols()` runs `getParseData()` on every `R/*.R` file in a project, extracts function definitions and call sites, and caches the results as RDS in the user cache directory. Cache invalidates on file content changes (MD5).

`blast_radius()` builds on top of `symbols()`. It finds internal callers, then scans `~/` for any project whose DESCRIPTION declares a dependency on the target package. Traces the call graph across all of them.

`projects()` scans for directories containing DESCRIPTION files and reads their metadata. `find_downstream()` does the same scan but filters to projects that depend on a specific package.

`briefing()` assembles project context from DESCRIPTION metadata, downstream dependents, Claude Code memory files, and recent git commits. It prints the briefing to stdout, returns the same text invisibly, and writes the markdown to the user cache directory so both the agent and user see the same context. An `agent` parameter controls memory inclusion: `agent = "claude"` skips Claude Code memory (since Claude Code autoloads it), while other values include it.

## Codex integration

Codex reads `AGENTS.md` files automatically before it starts work. This repo ships one at the root; for your own R projects, add rules like these so Codex reaches for saber instead of guessing:

```markdown
## saber Toolchain Rules

Before working on R code, use the right tool for the job:

| Situation | Command |
|-----------|---------|
| Understand a package's API | `r -e 'saber::pkg_exports("pkg")'` |
| Read function docs | `r -e 'saber::pkg_help("fn", "pkg")'` |
| Before renaming/changing a function | `r -e 'saber::blast_radius("fn", project = ".")'` |
| Understand a project's call graph | `r -e 'str(saber::symbols("."))'` |
| Discover R packages and deps | `r -e 'print(saber::projects())'` |
| What depends on a package | `r -e 'saber::find_downstream("pkg")'` |
| Project briefing | `r -e 'saber::briefing("project")'` |

**blast_radius is mandatory before renaming, moving, or changing the signature of any exported function.** It finds every caller across this project and all downstream projects. Skip it and you break things silently.
```

### SessionStart hook

saber ships a hook script that injects a project briefing into Codex at the start of every session. Find it with:

```r
system.file("scripts", "session-start.R", package = "saber")
```

Enable hooks in your Codex config (`~/.codex/config.toml`):

```toml
[features]
codex_hooks = true
```

Then add the hook to `~/.codex/hooks.json`:

```json
{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "startup|resume",
        "hooks": [
          {
            "type": "command",
            "command": "Rscript /path/to/session-start.R codex",
            "timeout": 15,
            "statusMessage": "Loading saber briefing"
          }
        ]
      }
    ]
  }
}
```

Every new Codex session starts with the project's metadata, downstream dependents, Claude Code memory (if available), and recent git commits already in context.

## Claude Code integration
Add the following to your `~/.claude/CLAUDE.md` to teach Claude Code how to use saber:

```markdown
### saber Toolchain Rules

Before working on R code, use the right tool for the job:

| Situation | Command |
|-----------|---------|
| Understand a package's API | `r -e 'saber::pkg_exports("pkg")'` |
| Read function docs | `r -e 'saber::pkg_help("fn", "pkg")'` |
| Before renaming/changing a function | `r -e 'saber::blast_radius("fn", project = ".")'` |
| Understand a project's call graph | `r -e 'str(saber::symbols("."))'` |
| Discover R packages and deps | `r -e 'print(saber::projects())'` |
| What depends on a package | `r -e 'saber::find_downstream("pkg")'` |
| Project briefing | `r -e 'saber::briefing("project")'` |

**blast_radius is mandatory before renaming, moving, or changing the signature of any exported function.** It finds every caller across this project and all downstream projects. Skip it and you break things silently.
```

### SessionStart hook

saber ships a hook script that injects a project briefing into Claude Code's context at the start of every session. Find it with:

```r
system.file("scripts", "session-start.R", package = "saber")
```

Then add it to your Claude Code settings (`~/.claude/settings.json`):

```json
{
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "Rscript /path/to/session-start.R claude",
            "timeout": 15
          }
        ]
      }
    ]
  }
}
```

Every new session starts with the project's metadata, downstream dependents, and recent git commits already in context. The `claude` agent flag tells `briefing()` to skip Claude Code memory (which Claude Code autoloads separately).

## License

Apache-2.0
