research-viz — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited research-viz (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Build (and resume) a reproducible Python research-visualization workspace against a tabular dataset. The deliverable is always a visualizations/ folder next to the user's data/, with three callable entry points (parse_input.sh, generate_plot.sh, interactive_page.sh) and human + agent-readable context in visualizations/info/.
There are six subskills inside this skill, and any single user request usually triggers one of them. Each lives in its own folder under `agents/<name>/AGENT.md` — read the matching AGENT.md before running the subskill.
| Subskill | Trigger | Read |
|---|---|---|
| parser | "parse / clean / load / read this", or any other subskill is requested but intermediate_data/ is empty | agents/parser/AGENT.md |
| plot_gen | "scatter of …", "violin per …", "correlation heatmap", "small multiples of …", "make me a figure" | agents/plot_gen/AGENT.md |
| interactive | "let me filter by …", "make a dashboard", "I want to brush / zoom", "show me a streamlit app" | agents/interactive/AGENT.md |
| style_infer | user uploads a paper / figure / brand guide and says "match this style", or specifies palette/typography/plot-type preferences in plain text | agents/style_infer/AGENT.md |
| significance_test | user asks for "t-test of X vs Y", "ANOVA across groups", "stat test", "p-values", "is this difference significant" | agents/significance_test/AGENT.md |
| domain_viz | user asks for visualizations outside the standard chart types and pointing at a specialized python package — EEG/fMRI/brain (mne, nilearn), molecular (py3Dmol), networks (networkx+pyvis), gene tracks (pyGenomeTracks), etc. | agents/domain_viz/AGENT.md |
Pick the subskill from the user's request. If the user is starting from raw files, run parser first; plot_gen and interactive both consume the parser's outputs (selected via intermediate_data/parsed_index.json's canonical_csv field) by default. If a visualizations/info/style_guide.md exists (produced by style_infer), every plot_gen and interactive run must read and follow it before generating anything new. significance_test and domain_viz are typically next-step prompts you offer the user, not things you run unprompted.
.py filesThe python scripts you produce are the project's recipe. After your session ends the user must be able to run bash visualizations/parse_input.sh, bash visualizations/generate_plot.sh --all, and bash visualizations/interactive_page.sh and reproduce byte-for-byte the same artifacts you produced — without re-prompting any agent, without remembering CLI flags, without copy-pasting --strategy '{"col":"median"}' from chat.
This means every choice you made during the session must be written into the python file as the default, not passed as a runtime flag. Specifically:
parser.py's PROJECT_STRATEGIES dict so bash parse_input.sh runs non-interactively and applies them automatically.parser.py (a clearly-named function near the top, e.g. apply_project_specific_cleaning(df)).data/ is awkward and you laid out a cleaner tree under intermediate_data/): encode the mapping in parser.py (e.g. a PROJECT_REORGANIZE function that maps each source path → desired parsed path), so the same reorganized layout falls out on every rerun.plot_gen.py's PROJECT_RECIPES dict (slug, prompt, data source, hue/palette/log-scale/figsize, etc.). Wire them up so bash generate_plot.sh --recipe <slug> reruns one and --all regenerates the lot..py file under streamlit/pages/ with the project-specific filters / charts / colors hardcoded. No runtime configuration needed.plot_gen.py (or in helpers/utils.py) and reference it everywhere — don't sprinkle hex codes inline.The scaffolding under assets/scaffolding/ is a starting template, not the final state. Different projects will end up with very different parser.py and plot_gen.py contents — that's the point. When the user later wants to change something (a different imputation strategy, a new plot, a different palette), an agent edits the python files; the user keeps using the same shell wrappers.
While a session is in progress: feel free to run things on the user's behalf via direct python calls or by passing flags. But before you wrap up, migrate every choice into the python files so the wrappers are self-contained going forward.
The scaffolded parser.py and plot_gen.py are starting templates that contain branches for every supported case (every plot kind, every missing-data strategy, multi-file modes, the prompt grammar parser, etc.). The version you deliver must not.
Once the project's behavior is settled:
violin / box / hist / line / heatmap branches from build_plot() (and the prompt-grammar lookup that picks between them — replace it with a direct call). If only median and drop_row are ever used, drop the unused branches in apply_strategy(). If the data is always a single file, remove the --combine plumbing entirely.re, seaborn-when-only-matplotlib-is-used, etc.). Run the script once to confirm nothing's referenced after the trim.PROJECT_RECIPES ends up with one recipe, the recipe-dispatch / --recipe / --all machinery is overkill — collapse to a single function and have the wrapper call it. The user's mental model should be: "this script does the thing I want, and the code inside is exactly the code that produced the thing."The principle: a human reading scripts/parser.py should see a tight, project-specific script — not a generic library with most of the code unreachable for their dataset.
Every function that remains, and every non-trivial step inside it, gets a concise comment explaining what it's doing for this project specifically. Aim for one short line per logical step — enough that a non-expert can follow what's happening without reading pandas docs.
Good (concise, project-specific):
# Drop rows where the patient gave no consent (consent column is "Y"/"N").
df = df[df["consent"] == "Y"]
# Convert visit_date from "DD-MM-YYYY" strings to datetime; rows that fail to parse
# are kept as NaT and handled later by the drop_row strategy on visit_date.
df["visit_date"] = pd.to_datetime(df["visit_date"], format="%d-%m-%Y", errors="coerce")Bad (generic, what-not-why):
# convert column to numeric ← obvious from the call
df["age"] = pd.to_numeric(df["age"])Bad (overlong essay):
# Here we use pd.to_datetime which is pandas' standard datetime parser. It accepts
# a format argument that follows the strftime conventions, see the pandas docs at
# ...
df["visit_date"] = pd.to_datetime(...)Comment the intent and why this dataset needs it, not the syntax. The combination of "trimmed code" + "explains what's happening per step" is what makes the file readable as a project recipe rather than a generic tool.
Several subskills (style_infer, the screenshot-feedback loop in plot_gen / interactive) involve looking at images. If the model running this skill doesn't have multi-modal capabilities, say so plainly and don't hallucinate image content. Concretely:
visualizations/info/style_refs/, but tell the user honestly you can't read it. Ask them to describe the relevant style features (palette, typography, plot kinds), or offer to exit the subskill.data/ is read-onlyThe skill never writes to, renames, or deletes anything inside data/. data/ is the user's source of truth — treat it as read-only at every step. All cleaned, reshaped, imputed, or otherwise transformed outputs go into intermediate_data/.
If the layout under data/ is awkward (e.g. mixed file types in one folder, opaque names like final_FINAL_v3.csv, or one giant flat dump that should be grouped by subject/session), do not reorganize the source. Instead, design a cleaner layout inside intermediate_data/ and write the transformed outputs there. Note the mapping (source → reorganized path) in info/context.md and in parsed_index.json (each per-file entry already records source_file and parsed_path).
Every file the parser (or any later transform) writes into intermediate_data/ must use a meaningful suffix that names the kind of transformation applied, joined to the source dataset name with a double underscore:
<original_dataset_name>__<stage>.<ext>Examples (tabular case — the default):
penguins__parsed.csv — basic load + quality check + missing-data strategies appliedpenguins__long.csv — pivoted from wide to longpenguins__imputated.csv — imputation pass distinct from __parsedpenguins__zscored.csv — standardized columnspenguins__filtered_adults.csv — row filter appliedThe extension follows the format the data actually needs. CSV is the right choice for the tabular data the standard parser handles, but for domain_viz workflows the format is whatever the domain package expects — forcing everything into CSV is silly and lossy when an established binary format exists. Use the native format and keep the same naming pattern:
subj01__parsed.fif — mne Raw / Epochs / Evoked written via .save(...)subj01__alpha_band.fif — band-passed and saved as fifbold_run1__resampled.nii.gz — nilearn 4-D NIfTI image after resamplingcomplex_AB__aligned.pdb — aligned protein structurecells__filtered.h5ad — scanpy AnnDatavolume__downsampled.zarr — napari / zarr chunked arrayPick the format the next step in the pipeline expects to load — never force a round-trip through CSV unless that's genuinely the cleanest representation. Record the format choice (and the reason, if non-obvious) in info/project_specific_knowledge.md and ensure each entry in parsed_index.json lists the actual parsed_path with extension.
Never use generic names like parsed_results.csv or cleaned.csv. The dataset prefix lets a human (or a future agent) glance at the folder and immediately see where each file came from; the suffix tells them what was done. When several stages stack on the same dataset, chain suffixes only if it materially helps (penguins__parsed__long.csv); otherwise keep the latest stage as the suffix and rely on parsed_index.json for the lineage.
The two reserved names produced automatically by the bundled tabular parser are:
<dataset>__parsed.csv — per-file output, one per input file (mirrors data/ layout, or a better layout you chose for intermediate_data/).combined__parsed.csv — only when the user explicitly asks to stack files (--combine concat or --combine both); has a __source__ column tagging each row's origin.A user may share their visualizations/ folder with another agent (or come back to it themselves a week later). That folder is the source of truth, not this conversation. The `info/` folder is the handoff layer — context.md is the recent-activity log, style_guide.md carries visual decisions, project_specific_knowledge.md carries domain learnings, how_to_use.md is the human-facing guide. Read all of them at the start; update the relevant supporting files during the work; write context.md last as the closing entry.
Be cautious: users edit files outside the agent loop. The context file can drift from reality. The protocol below handles that.
./visualizations/info/. If the whole visualizations/ doesn't exist, it's a fresh start — skip to Step 1 (Pre-flight).context.md — recent activity log, project-at-a-glance, drift signals.style_guide.md — always present (the scaffold ships a placeholder template). If the file's "Status" line says 🟡 Placeholder, no style decisions have been recorded yet and plot_gen is using its defaults; if 🟢 Active, follow the palette / typography / plot-type preferences + per-plot overrides exactly. Read this before writing or modifying any plot_gen / streamlit code, regardless of whether context.md flags it.project_specific_knowledge.md (if present) — domain-specific things a previous agent learned (e.g. how to use mne for EEG topomaps). May reference info/knowledge/<topic>.md for longer notes — read those if the user's request touches that topic.how_to_use.md — the human-facing guide. Skim so you don't contradict it; update it when the wrappers' behavior actually changes.ls visualizations/scripts/, ls visualizations/plots/, ls visualizations/intermediate_data/, ls visualizations/streamlit/pages/, ls visualizations/significance/ (if present), ls visualizations/info/style_refs/ (if present), ls visualizations/info/knowledge/ (if present) — and check data/ for new or removed files. Compare to what context.md claims.context.md (new files, missing files, modified scripts), surface the drift to the user in one short message before acting: "context.md says X was the latest plot, but I see Y/Z on disk and parser.py was modified — should I (a) update context.md to match reality, (b) re-run parsing, (c) ignore?" Then proceed based on the answer.While the work proceeds: update the relevant info/*.md as you go — style_guide.md when the user expresses a styling preference, project_specific_knowledge.md when you learn something domain-specific, how_to_use.md when behavior of the wrappers changes. Don't batch these to the end; they're work-product, not just documentation.
At the close of the session: write context.md last — append one concise entry per meaningful action (parser ran, plots produced, streamlit app updated, style_guide built or revised, significance test added, project_specific_knowledge appended). See assets/scaffolding/info/context.md for the format and tone. A future agent should be able to reconstruct what was done and where to find it from context.md alone, then load the supporting info/*.md files for depth.
The expected layout is:
.
├── data/ (the user's raw files — provided)
└── visualizations/ (this skill produces this)
├── README.md
├── info/
│ ├── context.md (recent activity log — written last)
│ ├── how_to_use.md (human-facing guide)
│ ├── style_guide.md (always present — placeholder template until style_infer fills it in)
│ ├── style_refs/ (always present; ships with a README; fills with verbatim reference papers / figures / brand guides when style_infer runs)
│ ├── project_specific_knowledge.md (created by domain_viz when a non-standard package was learned)
│ └── knowledge/ (only when project_specific_knowledge.md gets too long)
├── parse_input.sh
├── generate_plot.sh
├── interactive_page.sh
├── scripts/
│ ├── parser.py
│ ├── plot_gen.py
│ └── helpers/utils.py
├── intermediate_data/ (writable; data/ is never modified)
│ ├── parsed_index.json (always; manifest of what got parsed)
│ ├── <dataset>__parsed.csv (one per input file; suffix names the stage)
│ ├── <dataset>__long.csv (example follow-on transform)
│ ├── combined__parsed.csv (only with --combine concat / both)
│ └── <subdirs mirroring data/, OR a cleaner layout you designed if data/ is awkward>
├── plots/ (subfolder per plot prompt)
├── significance/ (created by significance_test — .txt tables of test results)
└── streamlit/
├── index.py
└── pages/Confirm before scaffolding (raise these questions with the user — concise, prefer recommendations over open-ended asks):
data/ directory at the cwd? If yes, use it. If no, ask the user where their raw files live (single absolute path or a list). Do not guess.data/ is large or contains many sessions/patients/clients, suggest the user split out a small pilot_data/ (a handful of representative files) and point the skill at that first. The wrappers honor DATA_DIR=..., so a typical workflow is: scaffold against pilot_data/, iterate on plots/dashboards until they look right, then re-run with DATA_DIR=$(pwd)/data. Surface this option whenever the data dir looks heavy (>~1 GB or hundreds of files) — don't force it on small datasets.python <skill-path>/scripts/scaffold.py <target-dir> --data-dir <data-path>. The scaffold script copies templates from assets/scaffolding/ and seeds the docs (README.md, info/context.md, info/how_to_use.md) with the data path. Read scripts/scaffold.py if you need to vary behavior.Decide the env before writing or running any Python.
which python3 && python3 --version. If a venv is already active in the current shell ($VIRTUAL_ENV) or there's a project-local .venv/, prefer that.ci= vs errorbar=, altair add_selection vs add_params, statannotations test signature, etc.) drift across releases, and an ImportError at write-time is much cheaper than a TypeError at the user's runtime. The minimum sets:pandas, numpy, openpyxl (for xlsx)pandas, numpy, matplotlib, seaborn (+ statannotations if a recipe overlays significance brackets)pandas, streamlit, plus altair if altair charts are requestedpandas, numpy, scipy (+ pingouin if used)See `references/env-management.md` for a one-shot version-detection snippet and a cheat sheet of known version-sensitive APIs in this stack — read it before reaching for any "this kwarg should exist" assumption. When the installed version is older than the cutoff for an API you wanted to use, write the legacy form rather than crashing on the user's machine.
pip install ...)visualizations/.venv and install there (skill will then update the .sh wrappers to source it)conda create -n <name> python=3.x ...)streamlit 1.28, altair 5.1, statannotations 0.6, …) so a future agent knows the surface they're targeting and can pick legacy vs. modern API calls without re-detecting.See references/env-management.md for the exact commands, the version-sensitive-API table, and the snippet that needs to go into parse_input.sh / generate_plot.sh / interactive_page.sh when a venv-based install is chosen.
Read the matching agents/<name>/AGENT.md before running. Each subskill file documents triggers, behavior, what to bake in, what to trim, and which references to consult.
agents/parser/AGENT.mdagents/plot_gen/AGENT.mdagents/interactive/AGENT.mdagents/style_infer/AGENT.mdCommon ordering:
info/style_guide.md.intermediate_data/ is empty, run parser before any plot/dashboard work.info/style_guide.md when it exists.info/The session closes in this order: supporting files first, `context.md` last.
extra field, so future plots stay consistent.domain_viz subskill drives this).mne topomaps").Keep context.md concise. The point is continuation — not a changelog. If the file grows past ~200 lines, summarize older entries into a "Summary so far" section at the top and trim.
When you need depth on a specific topic, read the appropriate file (don't preload them):
references/missing-data-strategies.md — strategies, when each is appropriate, common pitfalls.references/plotting-patterns.md — research-plot recipes in seaborn + matplotlib.references/streamlit-patterns.md — caching, multi-page apps, altair selections, large-data fallbacks.references/env-management.md — venv vs. conda, install commands, how to wire the venv into the .sh wrappers.references/figure-design-guidelines.md — the ten-rules-for-better-figures cheat sheet (Rougier et al. 2014). General design guidelines, not gates; useful when discussing chart choice with the user, when style_infer is making default choices, and when sanity-checking output.The scaffolding under assets/scaffolding/ is the single source of truth for the initial file contents — when in doubt, copy from there and adapt rather than writing from scratch.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.