DataStep Studio for R Install
Documentation · version 0.10.3

The manual

A chapter per feature, each with a short tutorial on one example study and then the reference. The same manual ships inside the extension (docs/manual); its examples are run by the extension's tests.

On this page
Chapters
  1. 1. Get started in five minutes
  2. 2. Run a program and read the log
  3. 3. The suspicious log
  4. 4. The data viewer
  5. 5. The debugger
  6. 6. pry: a stop written in the code
  7. 7. Trace a call, debug a pipeline
  8. 8. Format with its proof
  9. 9. Autocomplete and snippets
  10. 10. Coverage
  11. 11. Batch runs, tests and the clinical checks
  12. 12. Working with an AI agent: MCP and the CLI
  13. Which debugger when
  14. Questions
  15. Privacy

DataStep Studio for R is an extension for VS Code and Positron for statistical programming in R, by a person and an AI agent in one live R session. This manual has one chapter per feature. Each chapter starts with a short tutorial on the example study in examples/study (made-up data of 12 subjects in three arms), then gives the reference: keys, commands, options, and the dsr command an agent uses for the same thing.

The R tests run every example of this manual (test/r/test-manual.R): each line of output that a chapter shows is checked against a real run. When the tools change, the test fails until the manual says the new thing.

1. Get started in five minutes

From nothing to your first run, with the log that names each line.

What you need

Try it

  1. Install the extension. In VS Code: the Extensions view, search DataStep Studio for R, Install (or in a terminal: code --install-extension datastep-labs.datastep-studio-r). Positron, Cursor and VSCodium use another extension store: there, install the .vsix file (Extensions: Install from VSIX...), which the Marketplace page offers as Download Extension. Or with the installer script of the extension's folder install/ (it also checks R and the packages):

    sh install/install.sh --vsix datastep-studio-r-<version>.vsix
    

    On Windows: powershell -ExecutionPolicy Bypass -File install\install.ps1 -Vsix datastep-studio-r-<version>.vsix (not tested on Windows).

  2. Open a folder with R code. For this manual: docs/manual/examples/study. The DataStep R view appears in the activity bar.

  3. Start the R session. DataStep R: Start the R Session, or the play button in the Session view. The session runs in the project's folder, with the project's renv library when it has one.

  4. Check the machine. In a terminal of the editor: dsr doctor. Each problem comes with its fix.

  5. Run a program. Open 01_run.R and press Cmd/Ctrl+Shift+Enter. The R Console panel shows the log:

        5  NOTE: dm has 12 rows and 9 columns (was 12 x 8). New: BMIGRP (1 missing).
        7  NOTE: n_subjects = 12
    RESULT: CLEAN
    

    The verdict is CLEAN: no error, no warning. But the NOTE says that one subject got no BMI group. That is the planted bug of the example study; chapter 5 finds it.

  6. Look at the data. In the Session view, click dm: the data viewer opens with its rows from the session.

Next

2. Run a program and read the log

Every message at its line, and a NOTE for every data frame a step makes: you see what the program did without printing anything.

R prints little while a program runs. DataStep Studio for R runs a program expression by expression in the shared session and writes a log the way SAS does: the source lines with their numbers, then for each step its messages, warnings and errors at their line, and a NOTE for each data frame the step creates or changes (rows, columns, the new columns and their missing values). The last line is the verdict.

Try it

  1. Open examples/study/01_run.R and press Cmd/Ctrl+Shift+Enter (Run R File in the Session).

  2. Read the log in the R Console panel:

        3  NOTE: dm has 12 rows and 7 columns.
        4  NOTE: dm has 12 rows and 8 columns (was 12 x 7). New: BMI.
        5  NOTE: dm has 12 rows and 9 columns (was 12 x 8). New: BMIGRP (1 missing).
        6  NOTE: dm has 12 rows and 10 columns (was 12 x 9). New: AGEGRP.
        7  NOTE: n_subjects = 12
    Workspace: new: age_group (function (age)), bmi (function (weight, height)), bmi_group (function (bmi)), dm (data.frame 12 x 10), n_subjects (integer 1)
    RESULT: CLEAN
    
  3. Look at line 5: BMIGRP (1 missing). Every subject has a BMI, so every subject should get a group. A NOTE like this is often the first sign of a bug, even when the verdict is CLEAN.

  4. Run a part: select lines 4 and 5 and press Cmd/Ctrl+Enter. The objects of the session stay between runs, so after one full run you run only what you changed.

  5. Put the cursor on line 6 and choose Run to Line (Cmd/Ctrl+Alt+T): the program runs from the top to the cursor.

The verdict

Verdict Meaning What you do
RESULT: CLEAN no error, no warning, no suspicious message look at the result
RESULT: CHECK a warning or a suspicious message (chapter 3) read the FIRST CAUSE: fix it, or say why it is right
RESULT: ERROR the code failed at the line the FIRST CAUSE names fix the first cause only; its frames stay open (chapter 6)
RESULT: PRY the run waits at a stop (chapters 5 and 6) look, then go on

The NOTEs that matter most: a new column with missing values, an empty column (missing on every row), rows that grow in a join, a column whose type changed. The line before the verdict says what the run changed in the workspace.

Reference

Key or command What it does
Cmd/Ctrl+Shift+Enter run the R file in the session; on an error the cursor goes to its line
Cmd/Ctrl+Enter run the selection, or the line
Cmd/Ctrl+Alt+T, Run from Line run from the top to the cursor, or from the cursor to the end
Show Log of the Last Run the log as a page, each line number a link to its line
Stop the Running Code stop a long run (Ctrl+C); the objects of the session stay

A program runs in its own folder, as Rscript started there: its relative paths start at the program's folder.

For an agent

dsr run 01_run.R                    # the same log and verdict
dsr run 01_run.R --from-line 4 --to-line 5
dsr log --filter notes              # the NOTEs, warnings and errors only, each with its line
dsr run 01_run.R --json             # the result as data: events (each with its line), workspace, verdict

--timeout S comes back after S seconds while the run goes on (exit 3); dsr wait, then dsr log.

3. The suspicious log

A long log, and only the lines that matter: each with why it matters and how to fix it.

Some problems do not stop a program: text that becomes NA, a join that copies records, a column that changes its type. R says them once, as a warning that scrolls away, or not at all. The suspicious log collects them from the log of the last run, each at its line, with the rule that found it, why it matters and what to do. The run's verdict is CHECK when there is one.

Try it

  1. Open examples/study/02_suspicious.R and run it (Cmd/Ctrl+Shift+Enter). The verdict:

        4  WARNING: NAs introduced by coercion
        5  WARNING: adlb: rows went from 12 to 13 in a join: the other side has duplicate keys (check with dsr dups)
    RESULT: CHECK | FIRST CAUSE: 02_suspicious.R:4: NAs introduced by coercion
    
  2. Open the Suspicious Log view (in the DataStep R side bar). Each message is a line: click it, and the editor goes to its line. The same list is in the Problems panel; F8 goes from one to the next.

  3. The first message, rule [coercion]: a value of LBORRES is not a number (<5), so as.numeric() made it NA. Find it: dsr query lab --where 'is.na(AVAL)'.

  4. The second, rule [join]: the lab data have two baseline records of subject S-105, so the join copied a subject. dsr dups lab --key 'USUBJID LBTESTCD VISIT' shows the duplicate key.

  5. Fix the data or the program, or say why the message is right. A CLEAN verdict is the goal; a CHECK with a reason is fine.

The rules

dsr log --filter rules lists them all. The main ones:

Rule It finds
error the error that stopped the run
coercion values that became NA when converted (NAs introduced by coercion)
join a join that multiplies rows: duplicate keys on the other side
type a column whose type changed without a conversion in the code
recycling lengths that do not fit: a vector recycled
groups groups kept or dropped by a summary
masked a function masked by another package
factor a factor level that does not exist
condition a condition of length > 1
partial a partial name match (d$AG for d$AGE)
unknown_column a column that is not there
removed_rows rows removed because of missing values
empty_column a column that is missing on every row
missing_values a new column with missing values (information)
warning any other warning

For an agent

dsr log --filter suspicious         # each message with its line, rule, why and fix
dsr log --filter rules              # the rules
dsr run 02_suspicious.R --json      # data.suspicious: rule, line, why, fix

A harness that judges R runs itself can use the same rule: source("<dsr>/r/verdict.R"), then dsr_verdict(conditions).

4. The data viewer

Look at a data frame without writing a line of code, filter it with a click, and paste the filter into your program.

The data viewer shows a data frame of the session in pages of 100 rows; the data frame stays in R, so 150,000 rows stay quick. Every filter you make (a click on a value, a box under a column, a typed WHERE) is R code, and the viewer shows it as dplyr and base R that give the same rows.

Try it

  1. Run examples/study/03_viewer.R. In the Session view, click adsl. The viewer opens: 12 rows, the labels under the column names.

  2. Sort: click BMI; click again for descending. Shift+click AGE for a second sort column.

  3. A value into the WHERE: click ▾ next to ARM: its values with counts, distinct, missing. Click Placebo. The status line says 4 of 12 rows where ARM == "Placebo", and the filter is a chip above the grid (× removes it).

  4. A filter under a column: in the box under AGE type >=65 and press Enter. More forms: 1-5, a,b, !a, A* (starts with), . (missing).

  5. A cell: click a cell, press Enter: only the rows with its value. Shift+Enter: all rows but those.

  6. Find a column: press c and type body: the grid goes to BMI, whose label is "Body Mass Index (kg/m2)". A wide ADaM data set has 100 columns; you find a column by its label.

  7. Raw or formatted: press v. ARM is a factor: raw, it shows its codes (1, 2, 3); formatted, its levels. A labelled column (haven) shows its number or its label, a date its days or the date. A filter on a cell uses the value R compares, in either view.

  8. The view as R code: click R code. For the filter AGE >= 65, sorted by BMI descending:

    adsl |>
      dplyr::filter(AGE >= 65) |>
      dplyr::arrange(dplyr::desc(BMI)) |>
      dplyr::select(USUBJID, AGE, BMI)
    

    The viewer runs the base R form too and says "checked: it gives these rows".

  9. A record: click a row number: the row as a record, each column with its label. A USUBJID opens the subject across all data sets of the session.

Keys

Key What it does
click a column, Shift+click sort, a second sort column
▾ frequencies; click a value keeps it, Alt+click removes it, tick several and Enter
Enter / Shift+Enter on a cell keep / remove the rows with its value
Delete on a cell remove its column's filters
/ w g search all columns, the WHERE box, go to a row
c find a column by name or label
v raw or formatted values
l h labels on or off, hide the cell's column
? all the keys
Cmd/Ctrl+Alt+D the data frame at the cursor (adsl, study$adam$ADSL)
Cmd/Ctrl+Alt+F12 the last data frame a run made

Compare Two Data Sets compares two data frames value by value, by a key (PROC COMPARE).

For an agent

dsr query adsl --where 'ARM == "Placebo"' --vars 'USUBJID ARM AGE BMI' --n 4
dsr query adsl --raw --n 2                     # the stored values
dsr describe adsl --find body                  # columns by name or label
dsr query adsl --where 'AGE >= 65' --sort '-BMI' --vars 'USUBJID AGE BMI' --code   # the view as R code, checked
dsr freq adsl ARM; dsr dups lab --key 'USUBJID LBTESTCD VISIT'; dsr compare base comp --key USUBJID

The data tools take a data frame of the session, a file (.xpt, .sas7bdat, .rds, .csv), an object of a stop (@pry:x) or a step of a pipeline (@pipe[3]).

5. The debugger

Stop where a value goes wrong, look at every variable of every frame, and step: the R debugger of the editor, on the session you work in.

Press F5 on an R file, or click in the margin for a breakpoint. The run stops there and waits: the Call Stack view shows the frames with their file and line, the Variables view the objects of each frame (a data frame opens to its columns with their labels), and the Debug Console runs R in the frame. F10 steps over, F11 steps into a function of the project, Shift+F11 steps out, F5 goes on.

Try it: why does one subject get no BMI group?

  1. Run examples/study/01_run.R. Its log says New: BMIGRP (1 missing).

  2. Open R/derive.R. Click in the margin at line 11 (grp[bmi >= 25 & bmi < 30] <- "25-<30"). Right-click the red dot, Edit Condition, type any(bmi == 30): the run stops there only when a BMI is exactly 30.

  3. Open 04_debug.R and press F5 (or DataStep R: Debug R File). The run stops:

    R/derive.R:11: stopped at the breakpoint: where any(bmi == 30)
    
  4. In the Variables view, open bmi: one value is 30.0. In the Debug Console: bmi[bmi >= 29] gives 31.0 34.1 30.0.

  5. Press F10 (next): R/derive.R:12: stopped (next). Hover grp: the value of 30 has no group yet. Line 12 says grp[bmi > 30]: 30 is not greater than 30. The rule is >=30, so the line must say bmi >= 30.

  6. Press F5: the run ends. The log says NOTE: n_obese = 2: with the bug, the subject with a BMI of 30 is not counted. Fix line 12, run again: n_obese = 3.

More

Works in VS Code and in Positron (the same checks run in both: npm test, npm run test:positron). R runs one thing at a time, so Pause in the middle of a line is not possible: use a breakpoint, or Stop the Running Code.

For an agent

dsr run 04_debug.R --stop-at 'R/derive.R:11 if any(bmi == 30)'   # RESULT: PRY | AT: R/derive.R:11 ...
dsr pry eval 'bmi[bmi >= 29]'
dsr pry next                                                      # R/derive.R:12: stopped (next)
dsr pry continue                                                  # the rest of the run and its verdict

--stop-at can be given several times; --stop-on-warning stops at a warning. At a stop: dsr pry next [n] | step | finish | continue | quit, dsr pry break 'FILE:LINE if <condition>', dsr pry watch x.

6. pry: a stop written in the code

Freeze a real run where you want to look, also inside a loop, also when an agent runs it.

pry() in a program stops the run at that line, as %pry does in DataStep Studio for SAS. The run waits there: look at the objects of the frame, run code in it, then go on line by line or to the end. pry(when = <condition>) stops only when the condition holds. An error leaves its frames open the same way: the post-mortem pry.

Try it: stop for one subject in a loop

  1. Open examples/study/05_pry.R. Line 7 is pry(when = dm$USUBJID[i] == "S-110").

  2. Run it (Cmd/Ctrl+Shift+Enter). The run stops at subject S-110:

    05_pry.R:7: stopped at pry()
    

    The Pry panel opens: the stack, the code around the line, the objects of the frame, and an eval box.

  3. In the eval box: c(b, g). The BMI is 30 and the group is NA: the bug of chapter 5, found from the other side.

  4. Click next (or dsr pry next): 05_pry.R:8: stopped (next). The run goes on line by line in the loop.

  5. Click quit: the run ends here; the lines after it do not run (RESULT: STOPPED).

The commands at a stop

Command What it does
whereami, stack (bt), up, down, frame k where the run is; move between frames
ls, eval <code> the objects of the frame; R in the frame
next [n], step, finish, continue, quit go on: a line of this frame, into a function, out of it, to the next stop, end
break FILE:LINE if <cond>, watch <expr> one more stop; stop after a value changes
source <fn>, run <file.R>, pipe a function's code; a program run in the frame; the pipeline at the line
points, point k the snapshots of pry(stop = FALSE), a log point that does not stop

The data tools take the objects of the stop: dsr query @pry:dm --where 'is.na(BMIGRP)'.

An error keeps its frames: after RESULT: ERROR, dsr pry stack shows the frames from the top level down to the error, each with its file and line, and dsr pry eval runs code there.

For an agent

dsr run 05_pry.R                  # RESULT: PRY | AT: 05_pry.R:7: stopped at pry()
dsr pry eval 'c(b, g)'
dsr pry next                      # RESULT: PRY | AT: 05_pry.R:8: stopped (next)
dsr pry quit                      # RESULT: STOPPED

While a run waits, dsr status says PAUSED, the person sees the stop in the editor (the status bar), and dsr eval runs in the project folder. A new dsr run or dsr stop ends the run that waits.

7. Trace a call, debug a pipeline

Every line a function ran and what it changed, without one stop; and the step of a pipeline that lost the records.

Trace runs one call of a function and records every line that ran, the variables each line changed, and each if with its result and the values it compared. The pipe debugger runs a pipeline step by step and says, for each step, the rows before and after, the columns added, the values changed and the records it dropped (matched by USUBJID and the sequence number). They answer "why this value" and "where did the rows go" in one call.

Try it: trace a call

  1. Run 01_run.R (the session then knows bmi_group). Put the cursor on bmi_group in R/derive.R and choose Trace Function Call. A form opens with its argument bmi: type c(24.9, 30, 31) and click Trace.

  2. The trace lists lines 9 to 13. Under line 10: grp = [3] "<25", "NA", "NA"; line 11 changes nothing; under line 12: grp = [3] "<25", "NA", ">=30". The value:

    Value: [3] "<25", "NA", ">=30"
    

    The 30 never gets a group: line 11 stops at < 30, line 12 starts at > 30.

Try it: which step loses records?

  1. Open examples/study/06_pipe.R. It should keep the adverse events from the first dose day on, without the mild ones. Put the cursor in the pipeline and choose Debug the Pipeline at the Cursor (or the code lens above it).

  2. The steps:

       0  ae                                                            17 rows
       1  merge(dm[c("USUBJID", "ARM", "TRTSDT")], by = "USUBJID")      17 -> 17     +ARM +TRTSDT
       2  transform(ASTDY = as.numeric(as.Date(AESTDTC) - as.Date(TRTS  17 -> 17     +ASTDY
       3  subset(ASTDY > 1)                                             17 -> 9
            dropped 8 rows, e.g. S-101 / 3; S-102 / 1
       4  subset(AESEV != "MILD")                                       9 -> 6
    
  3. Step 3 dropped 8 rows. Click show them (or dsr query '@pipe[3]:dropped'): four of them have ASTDY 1, the first dose day. Day 1 is on treatment, so the step must say ASTDY >= 1. (The others are before the first dose: dropping them is right.)

For an agent

dsr trace --call 'bmi_group(c(24.9, 30, 31))' R/derive.R     # every line, what it changed, each if
dsr trace --call 'f(x)' --into g                            # also the lines of g, which f calls
dsr pipe 06_pipe.R:4                                         # the steps; then:
dsr query '@pipe[3]:dropped'                                 # the rows step 3 dropped
dsr data '@pipe[2]'                                          # the data after step 2

At a stop of the debugger or of pry, dsr pry pipe runs the pipeline of the current line on the frame's data. Which tool when: which-debugger.md.

8. Format with its proof

One key puts a program into the house style, and it is written only when a check proves that only the layout changed.

Format Document (Shift+Alt+F) lays out R code with styler in the project's style. Before it writes, it compares the program before and after, token by token: every token but the comments (its kind and its text) must be the same, and the comments word for word. When they are, it writes and says so; when they are not, nothing changes and it names the first difference. Strings, comments and code in roxygen comments never change.

Try it

  1. Open examples/study/07_messy.R (a function with mixed indentation, no spaces around <-).

  2. Press Shift+Alt+F. The status bar says:

    formatted (line_breaks): only the layout changed: the 35 tokens and 2 comments are the same
    
  3. Undo, then select lines 3 to 4 and choose Format Selection: only those lines change, at their indentation.

The scope: how much may change

dsr.format.scope / --scope styler may change
spaces spaces
indention spaces and indentation
line_breaks (the default) the layout: spaces, indentation, line breaks
tokens also tokens: ' to ", = to <-, braces. Written only when the parsed program and the comments are the same; styler adding braces to an if changes the program, and is refused

Settings

Setting
dsr.format.scope the scope above (empty: the project's, else line_breaks)
dsr.format.indentBy spaces per indent level (empty: the project's, else 2)
dsr.format.strict false keeps the spaces and line breaks styler would only tidy

The project's own style, for everyone who works on it: options(dsr.format = list(indent_by = 4, strict = FALSE)) in its .Rprofile. The editor's settings, when set, win over it.

For an agent

dsr format 07_messy.R --check          # would change, with the proof; writes nothing (exit 1 when a file would change)
dsr format R/ programs/                # write, file by file, each with its proof
dsr format R/f.R --scope tokens --indent-by 4 --strict false

On a large project: tried on the 49 R files of a clinical project, 35 would change at line_breaks and all 35 were proven layout-only.

9. Autocomplete and snippets

Autocomplete that knows R and your data: the columns of the data frame you work on, with their labels, as you type; and clinical programs from a short prefix.

When the session runs, the editor asks it what fits where the cursor is: the functions of the session and their arguments, the packages and their exports, the data frames, and the columns of the data frame a call or a pipe works on (also the columns a mutate() above made, and the data of an admiral exprs()), each with its label. Hover shows a function's help or a column's label and missing values; signature help shows each argument's help; F12 goes to the definition of a project function or a source()d file. Without a session (or while a run is busy) it uses the files: the project's functions, an index of the packages (dsr index), and the columns of specs/adam.

Try it

  1. Run 01_run.R. Open 08_complete.R, put the cursor after subset(dm, A on line 3 and press Ctrl+Space.
  2. The first suggestions are the columns of dm that start with A: ARM, AGE. A label shows next to a column when the data has one.
  3. Type adsl |> dplyr::filter( on a new line (after running 03_viewer.R): the columns of adsl with their labels.
  4. Type adsl-trt and press Tab: an admiral program for the treatment dates.

The snippets

Each snippet below is run by the R tests on the pharmaverse data, and the numbers of the table snippets are checked against an independent computation.

Prefix What it writes
dsr-header a program header: purpose, inputs, outputs, specification, QC
adsl-trt TRTSDTM/TRTEDTM and the dates from EX (admiral)
adsl-saffl the safety population flag from EX (admiral)
adsl-agegr age groups, with their numbers
adae-dates ADAE from AE: ADSL variables, start and end dates with imputation, study days, TRTEMFL (admiral)
bds-chg baseline flag, BASE, CHG, PCHG of a BDS data set (admiral)
adlb-param ADLB from LB: PARAMCD, AVAL, ADT, ADY, ANRLO/ANRHI/ANRIND, AVISIT (admiral)
adtte-param an ADTTE parameter: the first event or censored at the end of treatment (admiral derive_param_tte)
t-incidence subjects with an event by arm, as an analysis results data set (ARD)
t-summary n, mean, SD, median, min, max by arm, as an ARD
t-shell a table program from its shell: rows of a mock table filled by arm, as text and as an ARD
t-tte events and the Kaplan-Meier median with its 95% CI by arm (survival), as an ARD
pry-when pry(when = ...): a stop for one subject
test-that a testthat test

An ARD (stat, group, value, unrounded) is what dsr gate and dsr numbers compare.

For an agent

dsr complete 08_complete.R:3:20     # what fits there: columns with labels, arguments, exports, the signature
dsr syntax admiral::derive_vars_dt  # the arguments of the installed version
dsr index                           # the packages' functions for completion without a session

10. Coverage

Which lines ran, how often, and which never ran: a branch that never ran is a branch nobody tested.

A run with coverage counts how often each statement of the program and of the project's functions ran. The editor shows the count at each line (×3) and marks the lines that never ran. dsr test --coverage does the same for the tests: which lines of your functions the tests reach.

Try it

  1. Open examples/study/09_coverage.R and choose Run R File with Coverage.

  2. The log ends with the coverage of each file:

    Coverage: 09_coverage.R: 4 of 5 lines ran (never: 6)
    

    Line 6 (warning("an age over 100")) never ran: no subject is over 100. That is right here; in a test, it means the branch is not tested.

  3. Open R/derive.R: its lines carry their counts; the lines of bmi_group are marked as never run (this program does not call it). Clear the Coverage Marks removes them.

The counts are per statement: a branch inside one expression (if (n > 1) sd(x) else NA on one line) counts as one statement.

For an agent

dsr run 09_coverage.R --coverage
dsr coverage 09_coverage.R          # each line with its count; COVERAGE: 09_coverage.R: 4 of 5 lines ran (never: 6)
dsr test --coverage                 # the tests, then the coverage of the project's functions
dsr coverage R/derive.R

11. Batch runs, tests and the clinical checks

Proof, not a feeling: the program runs on its own, the tests pass, and the numbers of the table are those of the reference.

A program that ran in your session may still depend on something the session had. A batch run starts a new R session for each program, as a production run does, and writes its log next to it. The tests run in a clean R process. For clinical work, the checks compare what the programs made with what the study expects.

Try it

  1. Batch: in a terminal of the editor, dsr batch 01_run.R 10_table.R:

      CLEAN  01_run.R  log: 01_run.log
      CLEAN  10_table.R  log: 10_table.log
    BATCH: CLEAN
    
  2. Tests: dsr test (or DataStep R: Run the Tests). The example's test of the BMI groups fails, at its line:

      FAIL tests/testthat/test-derive.R:4: the BMI groups: every BMI gets a group, 30 is >=30
           x[4]: NA
           y[4]: ">=30"
    TESTS FAIL  (2 tests, 2 expectations in 1 files, 1 failed)
    

    Fix bmi_group() (chapter 5) and the tests pass. dsr test tests/testthat/test-derive.R runs one file; dsr test --coverage also says which lines of your functions the tests ran (chapter 10).

  3. The numbers of a table: 10_table.R writes output/t_ae.txt. reference/t_ae_ref.txt is the same table from a second programmer. Compare them number by number:

    dsr numbers output/t_ae.txt reference/t_ae_ref.txt
    
    output/t_ae.txt against reference/t_ae_ref.txt: 2 of 2 reference rows matched, 9 numbers compared
    NUMBERS: MATCH
    

    Rows pair by their label; each number is rounded to the reference's decimals. A difference names its row and both lines. A reference from a CSR made with SAS: --round sas. A PDF page: --page 49.

The checks

Check When Verdict
dsr batch <programs> a program must run on its own BATCH: CLEAN / CHECK / ERROR, a .log next to each program
dsr test testthat tests TESTS PASS / FAIL, each failure at its file and line
dsr numbers <output> <reference> an output table and a reference (a CSR page, a double-programmed table) NUMBERS: MATCH / DIFFERENT
dsr adam check <ds> ADaM programs with a specification specs/adam/<ds>.yml and a reference ADAM PASS / FAIL, cell by cell
dsr cdisc <data> data for a submission: names, labels, flags, dates, keys, codelists CDISC: PASS / WARNINGS / FAIL
dsr spec <ds> <var> one variable of a specification, against the data its derivation and codelist, checked
dsr app check, dsr gate a dashboard (Shiny, teal) with a contract dashboard.yml APP: OK / CHECK / ERROR, GATE PASS / FAIL

For an agent

Each check has a verdict line and an exit code (0 good, 1 not good), and --json gives the result as data. A step is done when the check says so: the guide (dsr guide) ends with them.

12. Working with an AI agent: MCP and the CLI

The agent works in your session with the same tools you use, and you see every run, log and stop it makes.

An agent (Claude Code, Codex, Cursor, Copilot, or your own) gets the tools of this manual as 44 dsr tools on the command line, or as typed tools of an MCP server. It runs R only through them, so it reads the same log, the same verdicts and the same suspicious messages you do, and its stops wait in the editor where you can look. The results are data: every tool takes --json.

Set it up

  1. In the editor: DataStep R: Set Up Agent Tools. It writes .mcp.json (Claude Code) and .vscode/mcp.json (Copilot) for the open project.

  2. Or by hand, for Claude Code:

    claude mcp add --scope project dsr -e DSR_ROOT=<project> -- node <extension>/bin/dsr-mcp.js
    

    Codex, Cursor and other clients: docs/agents.md.

  3. An agent with a shell needs no MCP: the editor puts dsr on the PATH of its terminals. Tell the agent to read dsr guide once: the working method on one page.

Try it with an agent

Open examples/study and ask the agent: "Some subjects have no BMI group. Find out why with dsr and fix it." The trial of this product (test/agent-trial) gives agents three tasks of this kind and grades the result. In the trial of 2026-11-12, a smaller model (Haiku) found and fixed a planted wrong derivation in 29 dsr calls: a stop with a condition (dsr run --stop-at '<file:line> if <condition>') and dsr pry eval showed the cause.

What the agent gets back

Field
verdict CLEAN, CHECK, ERROR, PRY, PASS, FAIL, MATCH, ...
summary the verdict line, e.g. RESULT: ERROR | FIRST CAUSE: prog.R:5: ...
fix what to do next, when dsr knows it
data the tool's result: the log's events with their lines, the rows of a query, the coverage, the proof of a format

Exit codes: 0 good, 1 not good, 2 a wrong call or no session, 3 still running after --timeout. In MCP the same object is structuredContent.

The session is shared

The agent panel

The Agent view of the extension runs Claude Code (with your own login) or a local model through Ollama, with the dsr tools only. See privacy for what each sends where.

Which debugger when

Four tools, one question each. They work together: a stop of the debugger or of pry can open the pipe debugger on its data, and step into the function the line calls.

The debugger pry Trace The pipe debugger
In one line Breakpoints in the margin, step by step. Your real run, stopped where you wrote it. Every line of one call, without a stop. Each step of a pipeline, with its rows.
The question "Where does this value go wrong, and what is in each frame?" "What is the data at this point of the real program, for this subject?" "Why does this function return this value?" "Which step loses or copies the records?"
You start it F9 in the margin, F5 (or Stop at Line) pry() or pry(when = ...) in the code, then run Trace Function Call (a form of the arguments) Debug pipeline above a pipeline
It shows the call stack, the variables of each frame (a data frame to its columns), the debug console, hover the stack, the code around the line, the frame's objects, an eval box each line that ran, what it changed, each if with its values rows before and after, columns added, values changed, the records dropped
Then next, step in, step out, continue, Run to Cursor, watch a value next, step, finish, continue, quit; the same as the debugger open the frames of an error the data after a step, the rows a step dropped, in the viewer
An agent uses dsr run --stop-at 'FILE:LINE if <cond>', dsr pry next dsr run, dsr pry eval, dsr pry next dsr trace --call 'f(x)' dsr pipe file.R:line
Chapter 5 6 7 7

An error needs no tool: its frames stay open after RESULT: ERROR. dsr pry stack (or the Pry panel) shows them, each with its file and line.

Which lines ran at all is the question of coverage (chapter 10), not of a debugger.

Questions

Each answer says what was tested, and says "not tested" where it was not.

Does it work in Positron? Yes. The editor checks (npm test: the session, runs, the debugger with breakpoints and Run to Cursor, pry, the call form, Problems, coverage, format, autocomplete, lint, the data viewer) ran in Positron 2026.11 (1.138) and passed, 22 of 22 (npm run test:positron). Positron has its own R console and its own R debugger; DataStep Studio's session is a separate R process, and its debugger takes the focus when its run stops. The data viewer's clicks and keys (npm run ui:check) ran in VS Code only.

Does it work with renv? Yes. The R session starts in the project folder, so the project's .Rprofile and its renv library are used. Tested with a dashboard project that has its own renv library (the checks of npm test).

Does it work with RStudio projects (.Rproj)? A folder with an .Rproj file opens like any folder, and the extension switches on there. The .Rproj file itself is not read, so its settings do not apply. Tested: a plain folder with R scripts (npm run test:plain); a folder with an .Rproj file: not tested.

Does it work on Windows? On Linux? Not tested. It was built and tested on macOS (Apple silicon). The code uses Node and R in ways that are not tied to macOS, and the Windows installer script exists, but neither Windows nor Linux has been tried.

Which R versions? Tested with R 4.4.1. R 4.4 or later is the stated need; older versions: not tested.

Do I need an AI agent? No. Every feature works without one. The agent uses the same tools; chapter 12.

Does it change my programs? Only when you ask: Format Document lays out the file you format, and only with its proof. Running and debugging do not change your files. The session keeps its own files in .triallens/ of the project (the session's address, the logs, plots); dsr batch writes a .log next to each program it runs.

Can I use it next to the R extension for VS Code (REditorSupport) or Positron's R support? They run their own R. DataStep Studio's session is another R process, so the objects of one are not in the other. Side by side with Positron: tested (above). Next to REditorSupport: not tested.

Does it work without the internet? Yes, everything but the optional parts named on the privacy page.

Cursor or VSCodium? Not tested. They install extensions from a .vsix file (Extensions: Install from VSIX...); the installer script takes --editor cursor or --editor codium.

Privacy

Your code and your data stay on your computer. DataStep Studio for R collects nothing: no telemetry, no usage data, no crash reports.

What talks to what

Part Talks to When
The R session listens on 127.0.0.1 only, on a random port, and answers only requests with its random token always, while it runs
The editor, dsr, the MCP server the R session on 127.0.0.1; the MCP server speaks to its client over stdin and stdout always
Check the Dashboard (dsr app check) a headless Chrome that opens your app on 127.0.0.1 when you run it
The Agent panel with Claude Code Claude Code runs on your computer with your login; what the agent reads (code, data it looks at, logs, screenshots) goes to Anthropic under your account only when you use the Agent panel with this setting
The Agent panel with Ollama Ollama at OLLAMA_HOST (default 127.0.0.1:11434): on your computer unless you point it elsewhere only when you choose it
New Dashboard Project, Set up R downloads the project's packages (renv) and the public CDISC pilot data only when you choose Set up
The dashboard that New Dashboard Project makes: its page your browser loads two fonts (IBM Plex Mono, Public Sans) from Google Fonts when you open the dashboard
The dashboard's Ask Claude tab your question, the data sets' variable names with labels and some values, and the result table (not the subject-level data) go to the model: by default Claude Code with your login; STUDYLENS_AI=ollama keeps it local, none turns it off only when you ask a question there
dsr check --cran R CMD check may reach CRAN, as it does without DataStep Studio only when you run it
Your own programs whatever they do when you run them

The installer scripts download the extension only from the URL you give them (--url); with --vsix they read a local file.

How this was checked

The editor itself (VS Code, Positron) has its own telemetry settings; they are the editor's, not this extension's.