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
- VS Code 1.85 or later, or Positron.
- R 4.4 or later, and
Rscripton the PATH (or the settingdsr.rscript). - Node.js, for the
dsrcommand that you and an agent type in a terminal. - The R packages of the session: httpuv, later, promises, evaluate, jsonlite, testthat, callr. styler for format,
lintr for more lint checks, covr is not needed (coverage is built in).
dsr doctorlists what is missing.
Try it
-
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.vsixfile (Extensions: Install from VSIX...), which the Marketplace page offers as Download Extension. Or with the installer script of the extension's folderinstall/(it also checks R and the packages):sh install/install.sh --vsix datastep-studio-r-<version>.vsixOn Windows:
powershell -ExecutionPolicy Bypass -File install\install.ps1 -Vsix datastep-studio-r-<version>.vsix(not tested on Windows). -
Open a folder with R code. For this manual:
docs/manual/examples/study. The DataStep R view appears in the activity bar. -
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.
-
Check the machine. In a terminal of the editor:
dsr doctor. Each problem comes with its fix. -
Run a program. Open
01_run.Rand 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: CLEANThe 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.
-
Look at the data. In the Session view, click
dm: the data viewer opens with its rows from the session.
Next
- Chapter 2 explains the log, chapter 4 the data viewer.
- To let an AI agent work in the same session: chapter 12.
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
-
Open
examples/study/01_run.Rand press Cmd/Ctrl+Shift+Enter (Run R File in the Session). -
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 -
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. -
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.
-
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
-
Open
examples/study/02_suspicious.Rand 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 -
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.
-
The first message, rule
[coercion]: a value ofLBORRESis not a number (<5), soas.numeric()made it NA. Find it:dsr query lab --where 'is.na(AVAL)'. -
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. -
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
-
Run
examples/study/03_viewer.R. In the Session view, clickadsl. The viewer opens: 12 rows, the labels under the column names. -
Sort: click
BMI; click again for descending. Shift+clickAGEfor a second sort column. -
A value into the WHERE: click ▾ next to
ARM: its values with counts, distinct, missing. ClickPlacebo. The status line says4 of 12 rows where ARM == "Placebo", and the filter is a chip above the grid (× removes it). -
A filter under a column: in the box under
AGEtype>=65and press Enter. More forms:1-5,a,b,!a,A*(starts with),.(missing). -
A cell: click a cell, press Enter: only the rows with its value. Shift+Enter: all rows but those.
-
Find a column: press
cand typebody: the grid goes toBMI, whose label is "Body Mass Index (kg/m2)". A wide ADaM data set has 100 columns; you find a column by its label. -
Raw or formatted: press
v.ARMis 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. -
The view as R code: click R code. For the filter
AGE >= 65, sorted byBMIdescending: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".
-
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?
-
Run
examples/study/01_run.R. Its log saysNew: BMIGRP (1 missing). -
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, typeany(bmi == 30): the run stops there only when a BMI is exactly 30. -
Open
04_debug.Rand press F5 (or DataStep R: Debug R File). The run stops:R/derive.R:11: stopped at the breakpoint: where any(bmi == 30) -
In the Variables view, open
bmi: one value is 30.0. In the Debug Console:bmi[bmi >= 29]gives31.0 34.1 30.0. -
Press F10 (next):
R/derive.R:12: stopped (next). Hovergrp: the value of 30 has no group yet. Line 12 saysgrp[bmi > 30]: 30 is not greater than 30. The rule is>=30, so the line must saybmi >= 30. -
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
- Hit count and log message: Edit Breakpoint takes a hit count (
3,>= 3,% 10) and a log message (BMI is {bmi}), which writes to the log and does not stop. - R warnings: tick R warnings in the Breakpoints view: the run stops where a warning happens, its frames alive.
- Watch a value: in the Variables view, Break on Value Change: the run stops at the line after it changes, and the stop says which line changed it.
- Edit a value: in the Debug Console,
bmi[3] <- 31changes the run from here on. - Run to Cursor: at a stop, right-click a later line, Run to Cursor.
- The pipeline at the stop: Debug the Pipeline at the Stop runs the pipeline of the current line step by step on the frame's data (chapter 7).
- Call a function with a form: right-click a function name, Call a Function with a Form: its arguments with their defaults and help; Debug starts a run that stops at its first line, Trace shows every line (chapter 7).
- Attach: when an agent's run waits at a stop, the status bar says "paused at ..."; Debug the Run That Waits at Its Stop opens it in the debugger.
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
-
Open
examples/study/05_pry.R. Line 7 ispry(when = dm$USUBJID[i] == "S-110"). -
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.
-
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. -
Click next (or
dsr pry next):05_pry.R:8: stopped (next). The run goes on line by line in the loop. -
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
-
Run
01_run.R(the session then knowsbmi_group). Put the cursor onbmi_groupinR/derive.Rand choose Trace Function Call. A form opens with its argumentbmi: typec(24.9, 30, 31)and click Trace. -
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?
-
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). -
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 -
Step 3 dropped 8 rows. Click show them (or
dsr query '@pipe[3]:dropped'): four of them haveASTDY1, the first dose day. Day 1 is on treatment, so the step must sayASTDY >= 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
-
Open
examples/study/07_messy.R(a function with mixed indentation, no spaces around<-). -
Press Shift+Alt+F. The status bar says:
formatted (line_breaks): only the layout changed: the 35 tokens and 2 comments are the same -
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
- Run
01_run.R. Open08_complete.R, put the cursor aftersubset(dm, Aon line 3 and press Ctrl+Space. - The first suggestions are the columns of
dmthat start with A:ARM,AGE. A label shows next to a column when the data has one. - Type
adsl |> dplyr::filter(on a new line (after running03_viewer.R): the columns ofadslwith their labels. - Type
adsl-trtand 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
-
Open
examples/study/09_coverage.Rand choose Run R File with Coverage. -
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. -
Open
R/derive.R: its lines carry their counts; the lines ofbmi_groupare 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
-
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 -
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.Rruns one file;dsr test --coveragealso says which lines of your functions the tests ran (chapter 10). -
The numbers of a table:
10_table.Rwritesoutput/t_ae.txt.reference/t_ae_ref.txtis the same table from a second programmer. Compare them number by number:dsr numbers output/t_ae.txt reference/t_ae_ref.txtoutput/t_ae.txt against reference/t_ae_ref.txt: 2 of 2 reference rows matched, 9 numbers compared NUMBERS: MATCHRows 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
-
In the editor: DataStep R: Set Up Agent Tools. It writes
.mcp.json(Claude Code) and.vscode/mcp.json(Copilot) for the open project. -
Or by hand, for Claude Code:
claude mcp add --scope project dsr -e DSR_ROOT=<project> -- node <extension>/bin/dsr-mcp.jsCodex, Cursor and other clients: docs/agents.md.
-
An agent with a shell needs no MCP: the editor puts
dsron the PATH of its terminals. Tell the agent to readdsr guideonce: 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 person sees the agent's runs in the R Console panel, its stops in the editor and the status bar.
- A run that waits at a stop answers
RESULT: PRY; the agent goes on withdsr pry next | step | continue | quit, or the person attaches the debugger to it. dsr stopcancels a long run; the objects stay.dsr statussays what runs.- A program's
rm(list = ls())does not end the session: the tools live in their own environment.
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 code: every place that can reach a network (
http.request,fetch,https, sockets,url(),download.file,curl,install.packages,renv::restore) was searched in the extension (src/), the CLI and MCP server (bin/) and the R tools (r/). The places found are those in the table: the session's server on 127.0.0.1, the CLI's and the editor's requests to it, Ollama's address, the dashboard's address on 127.0.0.1, the Set up command, and the install hints thatdsr doctorprints (it prints them; it does not run them). There is no telemetry library and no analytics call. - A running session: the session (R) was started, ran a program, a query, a format check, a completion and a trace,
and its network sockets were listed (
lsof -i -p <pid>on macOS): one,127.0.0.1:<port> (LISTEN).
The editor itself (VS Code, Positron) has its own telemetry settings; they are the editor's, not this extension's.