Repository navigation
feat(agents): R (ggplot2) code profile for the agent chat - #12443
Merged
Merged
Conversation
Pin tree-sitter 0.26.0 and tree-sitter-language-pack 0.13.0 in the agents extra (owner decision of 2026-10-29): 0.13.0 bundles its grammars, the R one is r-lib's (MIT), while 1.x downloads them at run time. agents/anyplot/code/treesitter.py parses once and maps tree-sitter's byte positions to the lines and character offsets the edit applier uses, lists ERROR and MISSING nodes as syntax problems, walks without recursion, and holds the R helpers: string decoding with escapes and raw strings, backtick names, callees, arguments with positions, assignments with the name they bind. It imports only the standard library and tree-sitter, so the R detection built on it can move to core/ later. The corpus test parses all 325 ggplot2 catalogue files with no ERROR or MISSING node. Nesting past tree-sitter's depth of about 1,024 levels is a syntax problem, so such a file fails closed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H7HSkGyea1k2v293XiAMo8
agents/anyplot/code/profiles/r/regions.py finds the ggplot2 conventions:
the exact theme statement (its default pinned to "light"), theme tokens
built from literals and earlier tokens (1,322 in the catalogue), the
Imprint palette, the protected save (the last top-level ggsave writing
sprintf("plot-%s.png", THEME) through ragg::agg_png), the Data and Plot
markers matched loosely (247 files carry both), column constants with a
role comment (4 files) and the placeholder df <- load_user_data(). It
imports only stdlib, tree-sitter, core.* and treesitter.py, so it can move
to core/ later; a test pins that boundary.
agents/anyplot/code/profiles/r/normalise.py strips the roxygen header,
inlines logistic-regression's bound-once output_file, blanks the catalogue
title with the Python profile's rule (now public as
normalise.is_catalogue_title; Python's output is unchanged) and notes the
five paste0 splits and the sprintf format, and rewrites only the protected
save's dpi so 34 saves (4800x2700, 2700x2700, 3600x3600) land on 3200x1800
or 2400x2400 with the layout in inches unchanged. The dpi is the shortest
decimal for which inches * dpi lies in [t, t + 0.5), checked in R's
floating point, so the canvas is exact whether ragg truncates or rounds.
All 325 catalogue files normalise idempotently onto a target canvas.
tree-sitter 0.26.0's Point.row and Point.column free an int past 256 under
Python 3.14 (a segfault at the next garbage collection, found on the
catalogue's line 271), so the modules index the point instead and a test
refuses the attribute form.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H7HSkGyea1k2v293XiAMo8
agents/anyplot/code/profiles/r/validate.py walks the tree-sitter tree with the rules of the design pass of 2026-10-29, reusing the Python profile's Finding and rule ids. SECURITY: denied names as identifiers anywhere and as strings equal to them (the design's list plus file, editor and environment routes, without words common as column names such as dir or browser); library() and pkg:: only on the owner's allowlist with per-package rules (utils::head, tools::toTitleCase, gridExtra::arrangeGrob, ragg::agg_png only as the save's device); ':::' refused; string and parenthesised callees refused; do.call only with a bare rbind, cbind or c; function-valued arguments only literal functions, lambdas, literals, names bound only to functions or factories over them; !! and !!! only on column constants, := and glue names checked; facet and formula strings parsed and walked as code; file = refused except pdf(NULL); <<- and ->> refused; the protected save pinned and THEME, ggsave, sprintf, Sys.getenv and the tokens bound only where they belong. ADAPTATION: placeholder count and use, random draws (runif, sample, set.seed and jitter stay), literal data (c() over 20 numbers, data(), built-in datasets) and the palette prefix. The named bypasses of the design pass are tests. Over the 325 normalised catalogue files SECURITY flags 15: 4 map specs, qrcode-basic, the dataset packages of dendrogram-basic and scatter-categorical, maze-circular's <<- and 7 files whose flow data use a column or variable called source. The Python validator is untouched. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H7HSkGyea1k2v293XiAMo8
agents/anyplot/code/profiles/r/coupling.py holds the R detection that is meant to move to core/ (stdlib, tree-sitter, core.*, treesitter and the R regions only, pinned by a test): the structural reasons (syntax, no-theme, savefig-target) and the coupling categories data-block, synthetic-data, limits, color-scale, annotations, statistics and palette. A file is clean with both markers and role-commented column constants and nothing outside its Data block; synthetic data inside a complete block is the block the adapter swaps, and a bare set.seed beside it couples nothing. profiles/r/readiness.py adds the SECURITY validator and the wording, with the Python scan's Readiness result and reason format: blocked only for real reasons (map spec, library disabled, syntax, security, no theme, save), a missing marker never blocks (owner decision of 2026-10-29). The Python scan's formatting helpers became public (snippet_of, clip_reason, format_hint) so both share them; its output is unchanged. Over the 325 normalised catalogue files: 4 clean (area-basic, box-basic, line-basic, line-timeseries), 297 coupled, 24 blocked (13 map specs, qrcode-basic, 7 files with a 'source' column, the gapminder and palmerpenguins dataset packages, maze-circular's <<-). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H7HSkGyea1k2v293XiAMo8
… line and R error grammar code/edits.py becomes one core for matching, overlap, drift and restore with a Dialect per language (dialect_for): Python reads with ast and compares ast.dump as before; R reads with tree-sitter, protects THEME, the tokens, df <- load_user_data() and the final ggsave, compares the token stream with decoded strings, counts R bindings (any arrow, replacement forms, parameters, for variables) and lets the whitespace tier read backticks, strings that span lines and raw strings. The Python messages and results are byte-identical (test_edits.py unchanged). data/kinds.py derives the value kinds a typed reader needs from the canonical data.csv: integer or big-integer (past 32 bits), number, boolean, text, date (every cell a plain ISO date) or datetime. profiles/r/loader.py replaces the placeholder with readr::read_csv typed from them (col_integer, col_double, col_logical, col_character, col_date, col_datetime in UTC), na = "", one thread, no progress, eager. The Python loader is untouched. code/export.py takes the run command (python plot.py or Rscript plot.R). profiles/r/errors.py reads R's stderr: the erroring function of base R's and rlang's error lines, the chained cause and ggplot2's layer ordinal, the last three Calls: names, and fixed texts for memory, recursion and a missing package; never the message. Plus the changelog fragment for the R profile. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H7HSkGyea1k2v293XiAMo8
ancestors() and field_of() walked Node.parent, which visits() replaced with one pass that carries the parent and field along; nothing calls them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H7HSkGyea1k2v293XiAMo8
…ose the formula gap Owner decisions of 2026-10-29 on the first pass's findings: - A string equal to a denied name is refused only where a string becomes a function: as a callee, as a function-valued argument (named, or the positional slot of the apply family, Map/Reduce/Filter, outer, the purrr map family, across) and inside a parsed code string. Everywhere else it is data, so a user's column called source, file, load, options or body passes. - A bare denied name is refused as a callee and as a function-valued argument everywhere, and as any other value outside a data mask. Inside the arguments of the data-masking and tidy-select calls (DATA_MASK_CALLS: aes, vars, the facets, the dplyr and tidyr verbs, with/subset) and in the operands of ~ it is a column. The accepted residual is written down: a function object can be stored in a column inside a mask and called through a lambda's parameter; the sandbox and the exec filter are the boundary. - gapminder and palmerpenguins pass SECURITY and become the ADAPTATION finding dataset-package, a synthetic-data hint in the readiness scan. - A formula position (formula = and the first positional of lm, glm, nls, loess and the other FORMULA_FIRST models) takes only a ~ literal, a string literal or a name bound once to one; the formula-or-data functions (aggregate, the *.test functions, prcomp) refuse a string built at run time (paste0, sprintf, format, glue, ...). The 7 flow files and the 2 dataset-package files are coupled now: 4 clean, 306 coupled, 15 blocked (13 map specs, qrcode-basic, maze-circular). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H7HSkGyea1k2v293XiAMo8
Measured in the toolchain image (R 4.6.1, readr, ragg) on 2026-10-29: ragg truncates (13.333 in at 240 dpi gives 3199 px), so the normaliser plans the dpi for t <= inches * dpi < t + 1 and measures floor(inches * dpi). The corpus test computes floor(inches * dpi) from every rewritten save's own literals and asserts the target in both dimensions. col_double() reads 9007199254740993 as ...992, so data/kinds.py gains read_kinds() with ColumnKinds.inexact: per column, the integer cells beyond +-2^53, for PR 3b to announce under the owner's data-fidelity rule. column_kinds() stays as before. The loader's docstring records the other measurements (True/False, microseconds, offsets to UTC, text NA and 001, empty cells). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H7HSkGyea1k2v293XiAMo8
…ents image tree-sitter-language-pack 0.13.0 bundles about 170 compiled grammars (184 MB installed). The agents image, the only one that installs the agents extra (the renderer installs the lib-* extras), now deletes every grammar but TREE_SITTER_GRAMMARS (R) after uv sync and fails the build when a kept grammar no longer loads: 0.6 MB left. A test ties the list to GRAMMARS in treesitter.py, for every Dockerfile that installs the extra. Verified by building the builder stage locally (r.abi3.so alone, the import check passes) and with hadolint 2.15.1 at the warning threshold (no new finding). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H7HSkGyea1k2v293XiAMo8
…changelog The spec's step 2c records what PR 3a built and the owner decisions of 2026-10-29, and marks the plan's "the readiness scan blocks a file without the marker" superseded: in every language a missing marker means coupled, never blocked, so no language step waits for the Data-block regenerations. It also names what 3b must wire: dataset-package is an ADAPTATION finding, which the pipeline treats as a defect line, so the R runtime's blocking set needs it for the working form to be refused. docs/concepts/agent-network.md gains a short R-profile paragraph under "Adapt and validate", and the changelog fragment carries the narrowed denied-name rule, ragg's truncation, the 2^53 count and the pruned image. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H7HSkGyea1k2v293XiAMo8
…-function calls Review of a12dd3e (Fable on security, Gemini 3.8 Flash on the diff): - THEME, the tokens and the save's names were checked by line, so 'THEME <- Sys.getenv("ANYPLOT_THEME", "light"); THEME <- "dark"' passed. The R regions now carry each statement's exact span, the validator counts a binding only when it is that statement, and every protected statement (theme, tokens, palette, placeholder, save) must have its lines to itself, which also closes 'df <- load_user_data(); df <- iris'. - The data-mask residual was wider than written. with and within are no mask any more, and the calls that would run a function held as a value are refused, after measuring the catalogue: a function literal called at once (0 uses), an index or field as callee (1, plotmath in expression(), exempt), a call to a parameter of an enclosing function literal (0), do.call as a value, and a call's result unless it is a builder (CALLABLE_FACTORIES; the 12 catalogue uses are colorRampPalette and scales labels). .mapply, forceAndCall, nlm and nlminb gained function slots. The residual paragraph now says what still passes. - transform = and inverse = accept scales' transformations (transform_*(), *_trans(), trans_new()), as labeller = its builders. - do.call also takes "rbind", "cbind", "c" as strings and paste, paste0. - textConnection and rawConnection are denied names. The split is unchanged: 4 clean, 306 coupled, 15 blocked. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H7HSkGyea1k2v293XiAMo8
… coupling scan coupling.py flagged 'iris <- df[...]' and later uses of iris as synthetic data, while the validator skips names the file binds; both read it as the file's own variable now (Gemini review of 2026-10-29). The spec's step 2c records the review's fixes and the remaining residual. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H7HSkGyea1k2v293XiAMo8
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H7HSkGyea1k2v293XiAMo8
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
Contributor
There was a problem hiding this comment.
🔵 Needs a closer look
Human review is warranted because the change combines a security-sensitive R validator and grammar-pruned image path with deferred runtime and sandbox integration that cannot be verified end to end here.
No open issues 🎉
What changed in this PR
This pull request adds the deterministic R/ggplot2 code-profile foundation for the agent chat’s future “use with my data” flow. Runtime and registry integration remain deferred to a follow-up PR.
Changes:
- Adds R-specific parsing, protected regions, normalization, validation support, readiness scanning, data loading, error handling, editing, and export infrastructure.
- Generalizes shared editing and export helpers while preserving Python behavior.
- Adds pinned tree-sitter dependencies, Docker grammar pruning, corpus tests, documentation, and changelog updates.
| File | Description |
|---|---|
uv.lock |
Locks tree-sitter and language-pack dependencies. |
tests/unit/agents/code/test_treesitter.py |
Tests parsing, positions, R literals, assignments, grammar constraints, and catalogue parsing. |
tests/unit/agents/code/test_profile_r_regions.py |
Tests R protected regions, markers, theme tokens, saves, and columns. |
tests/unit/agents/code/test_profile_r_readiness.py |
Tests readiness classification, coupling hints, and corpus counts. |
tests/unit/agents/code/test_profile_r_normalise.py |
Tests R header, title, save-target, and canvas normalization. |
tests/unit/agents/code/test_profile_r_loader.py |
Tests typed readr loader generation and column-kind detection. |
tests/unit/agents/code/test_profile_r_errors.py |
Tests safe R error summaries and resource-error classification. |
tests/unit/agents/code/test_profile_r_edits.py |
Tests R dialect editing, protected drift, restoration, and export behavior. |
tests/unit/agents/code/conftest.py |
Adds R catalogue fixtures and corpus inputs. |
pyproject.toml |
Adds pinned tree-sitter dependencies to the agents extra. |
docs/concepts/agent-network.md |
Documents the staged R profile and its security and readiness behavior. |
changelog.d/r-agent-profile.md |
Adds the changelog fragment for the R profile foundation. |
agents/Dockerfile |
Prunes the language-pack image to the grammars used by active profiles. |
agents/anyplot/data/kinds.py |
Derives R-specific column kinds and large-integer precision warnings. |
agents/anyplot/code/readiness.py |
Shares readiness formatting helpers with language profiles. |
agents/anyplot/code/profiles/r/regions.py |
Detects R theme, palette, marker, placeholder, column, and save regions. |
agents/anyplot/code/profiles/r/readiness.py |
Classifies R files as blocked, coupled, or clean. |
agents/anyplot/code/profiles/r/normalise.py |
Normalizes R catalogue headers, titles, save targets, and canvas DPI. |
agents/anyplot/code/profiles/r/loader.py |
Generates typed readr::read_csv loader code. |
agents/anyplot/code/profiles/r/errors.py |
Summarizes R stderr without echoing user data. |
agents/anyplot/code/profiles/r/coupling.py |
Detects data, synthetic-data, scale, annotation, statistic, and palette coupling. |
agents/anyplot/code/profiles/r/__init__.py |
Defines the staged R profile package. |
agents/anyplot/code/profiles/__init__.py |
Documents the non-Python code-profile package. |
agents/anyplot/code/normalise.py |
Exposes the shared catalogue-title predicate for R normalization. |
agents/anyplot/code/export.py |
Adds language-specific export run commands, including Rscript plot.R. |
agentic/specs/261024-agent-all-libraries.md |
Records the R profile scope, implementation plan, and owner decisions. |
Review effort: Lite
7 of 9 tasks
MarkusNeusinger
added a commit
that referenced
this pull request
Oct 29, 2026
## Summary - `RRuntime` (`agents/anyplot/render/runtimes/r.py`) serves ggplot2 through the runtime registry on top of PR 3a's R profile (#12443). - The registry dispatches on the library's language. - The contract gains the column kinds for the loader, per-runtime blocking rules (R adds `dataset-package`, so a working form that keeps gapminder or palmerpenguins is refused) and adaptation targets. - The R row runs in exec mode under the exec filter, with a repair reserve of 15 s. - `agents/cloudbuild.yaml` adds ggplot2 to the admin deploy's `_LIBRARIES`; the 80 % eval matrix still gates the public release. - **Prompts.** - `adapter.md` is split into shared rules with slots, filled by `adapter_python.md` and `adapter_r.md` (and likewise `adapter_highlights.md`). Python's assembled prompts are pinned byte-identical by SHA-256. - Matplotlib/probe-only lines moved into Python's slots; R's say an R render has no probe. - The scope judge and the fixed `other_library` reply (11 languages) were stale since 2026-10-25: they still said only matplotlib and seaborn. They now name the served libraries from `AGENT_LIBRARIES` through a `{{libraries}}` slot, so a request for this plot in a served library is in scope. - **Data fidelity** (owner decision of 2026-10-29): when R reads integer cells beyond ±2^53 as the nearest double, the version's "what changed" list names each column with its count, and the plot stays `ok`. - **Evals:** 60 ggplot2 fixtures for the 10 matrix specs × 6 perturbations, plus the smoke case `scatter-basic-ggplot2-renamed`. Eligibility and the dev fixture find files by the library's language. ggplot2: 310 of 325 eligible (4 clean, 306 coupled, 15 blocked). - **Frontend fix:** a result card highlighted every version in the page's language, so after a library switch an older matplotlib version showed as R or the other way round. It now uses the version's own library. ## Plan `agentic/specs/261024-agent-all-libraries.md`, step 2c ("What PR 3b built"). ## Test plan - [x] `uv run ruff check .` and `uv run ruff format --check .` - [x] `uv run --extra typecheck --extra agents mypy api core agents`: no issues in 167 files. - [x] `uv run pytest tests/unit tests/integration -n 6`: 19,273 passed, 27 skipped. - [x] App: `yarn lint`, `fm:check`, `type-check`, `test` and `yarn build` on Node 24. - [x] Browser check with the BFF mock (`AGENT_LIBRARIES=matplotlib,ggplot2`): after a plot in matplotlib, a switch to ggplot2 and a second plot, version 1 shows `language-python` and version 2 `language-r`. The only console errors are the known font CORS errors on port 3002. - [x] Local render without a model: an identity adaptation of `line-basic-ggplot2-renamed` through `LocalDockerBackend` on the renderer image gives 3200x1800 in both themes, R1–R3 pass, and `exec_deny: on`. - [x] Gemini 3.8 Flash checked the diff before the PR. All three findings were checked and none needed a change: - the heredoc in `ci-image.yml`: the YAML block strips the indentation, and `bash -n` passes on the extracted step; - the `refusals()` cache: process-wide, like `get_settings`; - the note length: bounded by the 64-character column cap. - [ ] `.github/workflows/ci-image.yml`: the new agents-image step (the R grammar loads, an R readiness scan runs in the built image) has no local loop; its first real run is this PR's CI. - [ ] After merge and deploy, two owner steps: the ggplot2 eval matrix on Vertex, then the first live admin test, which confirms the readr types end to end. --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
agents/anyplot/code/profiles/r/: tree-sitter parsing (tree-sitter0.26.0,tree-sitter-language-pack0.13.0, pinned in theagentsextra), the protected regions, the normaliser, the SECURITY and ADAPTATION validator, the readiness scan, the readr loader with column kinds (data/kinds.py) and R's error grammar. Step 2c of the all-libraries plan, PR 3a. It is not served yet:RRuntime, the registry entry, the prompts and the eval fixtures come in PR 3b.# Datamarker makes a filecoupled, neverblocked. On the 325 catalogue files the split is 4 clean, 306 coupled and 15 blocked (the 13 map specs, qrcode-basic, and maze-circular for its<<-). Other owner decisions: the catalogue title is blanked to""as in Python, the canvas fix rewrites only the save'sdpi(ragg truncates, which was measured and is modelled), and onlygridExtra::arrangeGrobis allowed.code/edits.pygets a language-neutral core with aDialectper language, andcode/export.pytakes the run line as a parameter. Python's behaviour is unchanged and its tests are untouched.agents/Dockerfilekeeps only the R grammar of the language pack (0.6 MB of 184 MB).Plan
agentic/specs/261024-agent-all-libraries.md, step 2c ("What PR 3a built"). The "readiness blocks a file without the marker" text is marked superseded.Security
The sandbox (gVisor) and the exec filter (
execdeny, proven by a preflight before user code) are the boundary. The static validator is hygiene for the exportedplot.R.Before this PR opened, Fable reviewed the validator (verdict: sound, no high findings) and Gemini 3.8 Flash checked the diff. Every confirmed finding is fixed in this branch:
THEME <- ...; THEME <- "dark"passed. Bindings are now counted by node, and every protected statement owns its lines.with/withinare no longer masks, and immediately invoked function literals, subset and$callees, calls of a function-literal parameter, unknown call-result callees, anddo.callas a value are refused. None of these refusals hits a catalogue file.transform = scales::transform_log()anddo.call("rbind", ...)were refused and are now accepted.textConnectionandrawConnectionare denied names.A denied name is refused where it can run: as a callee, as a function-valued argument, or as a value outside a data mask. A user column called
source,fileorloadtherefore passes. The accepted residual is documented in the validator's module docstring.Notes for PR 3b
dataset-package), butpipeline.py'sBLOCKING_RULESdoes not refuse them yet.read_kinds()counts integer cells beyond ±2^53, which R reads inexactly, so the session can announce them.True/False, microseconds, offsets to UTC, and textNA/001.nchar(title). With the blank""title they size it for an empty string until the adapter writes a title. No workaround was added.Point.row/.columnis read above 256. The code uses tuple indexing, and a test guards it.Test plan
uv run ruff check .anduv run ruff format --check .uv run --extra typecheck --extra agents mypy api core agents: no issues in 166 files.uv run pytest tests/unit tests/integration -n 6: 19,223 passed, 27 skipped.plots/*/implementations/r/ggplot2.R: every file parses with no ERROR node, the normaliser is idempotent,floor(inches * dpi)hits the target in both dimensions, and the readiness split asserts the exact counts.uv run python -m tools.changelog check --base origin/mainanduv lock --check.agents/Dockerfilebuilder stage was built locally: onlyr.abi3.sois left and the grammar import check passes.