sdrf-skills
CLAUDE.md19 starsChanged 7 days agoMITView on GitHub
A skills-first repo: the product is skills//SKILL.md — markdown workflows that teach an AI assistant to annotate proteomics SDRF files. Supporting Python lives in tools/. Specification data is *not** stored here; it is read at runtime from the spec/ git submodule. Skills are auto-discovered — .claude-plugin/plugin.json carries no skills key, so Claude Code scans the skills/ directory automatically. Each SKILL.md declares its own name and routing description in frontmatter. Currently 4: sdrf-annotate, sdrf-contribute, sdrf-campaign (invoked as /sdrf-skills:sdrf-annotate etc. once installed…
- Installs packages
We split this CLAUDE.md into the rules it gives an agent
4 rules. About 0 are common, 0 are distinctive, and 4 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
4
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 20
Run ls skills/ for the current set — do not trust a hardcoded skill count anywhere in this repo.
- Very specific: line 106
No skill uses allowed-tools, model, or version.
- Very specific: line 145
ONLY by a short page; an empty page after a full one is ambiguous, so it triggers a year-partitioned (filter=submissionDate==YYYY) retry …
- Very specific: line 151
Article identifiers: one parser — silent-corruption class: Every article tool classifies its input with _parse_article_id: bare forms, …
≈ “Common” is an estimate: at least 1 in 5 of the 25,680 CLAUDE.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
What this repo is
A skills-first repo: the product is skills/*/SKILL.md — markdown workflows that teach an AI
assistant to annotate proteomics SDRF files. Supporting Python lives in tools/. Specification data
is not stored here; it is read at runtime from the spec/ git submodule.
Skills are auto-discovered — .claude-plugin/plugin.json carries no skills key, so Claude Code
scans the skills/ directory automatically. Each SKILL.md declares its own name and routing
description in frontmatter. Currently 4: sdrf-annotate, sdrf-contribute, sdrf-campaign (invoked as
/sdrf-skills:sdrf-annotate etc. once installed as a marketplace plugin) plus sdrf-adversarial-review,
which sdrf-annotate dispatches into a fresh context at Step 9.5 and which is never typed.
sdrf-annotate is the one entry point for everything about one SDRF - create (review always
included), review/validate/check/score/fix an existing file, look up cell lines and terms, verify
technical metadata from raw files, plan, explain. Its references/ hold what used to be separate
skills (setup, knowledge, templates, validate, fix, techrefine, cellline, review, design).
Run ls skills/ for the current set — do not trust a hardcoded skill count anywhere in this repo;
README.md alone carries three contradictory numbers, and commit d5c4c70 exists only to repair drift.
Commands
# Tests (126, ~2s, no network). CI runs: python -m pytest tests/ -v --tb=short
python -m pytest tests/ -q
python -m pytest tests/test_parser.py -q # single file
python -m pytest tests/test_services.py::TestPRIDEClient::test_get_project_mock -q # single test (tests use classes)
python -m pytest tests/ -q -k unimod # by keyword
# Lint — NOT in CI, and currently RED: 32 errors (28 F401, 4 F541), all auto-fixable.
# tools/review_gate.py is clean; the rest predates it. Run `ruff check --fix` before gating on it.
ruff check tools/ tests/
# Submodule: TWO levels deep (spec/ -> spec/sdrf-proteomics/sdrf-templates).
# `--recursive` is mandatory or templates.yaml is silently MISSING.
git submodule update --init --recursive # restore pinned state (what you usually want)
git submodule update --remote --recursive # advance to upstream tip; leaves a dirty gitlink
# tools CLI: `sdrf-tools` console script after `pip install -e .`; `python -m tools` also works from the repo root
sdrf-tools --help # check, structure, contract, build, score, fix, benchmark, massive-files,
# verify, cellline, review-gate, reconcile, audit-existing, curated,
# bruker-dia, search-paramspython is an alias to python3 here, not a binary — skills invoke bare python, assuming an
activated env. Supported: Python 3.10/3.11/3.12 (CI matrix); environment.yml pins only
python>=3.10 with no upper bound. conda is not installed on this machine, so the README's
"recommended" conda path does not work here; uv is available and requirements.txt already assumes it.
Architecture
Three layers, loosely coupled — the coupling gaps matter more than the layers:
skills/— 4 SKILL.md workflows.sdrf-annotateis a ~3.7k-word core plus seventeenreferences/files it reads on demand (gathering, sample and technical values, templates, validation, fix patterns, cellline, techrefine, review checks, reconcile, format rules, explaining, OLS lookup, setup, planning);references/format-rules.mdis the single canonical copy of the format rules; the two review-gate skills shipreferences/review-contract.mdandagents/openai.yaml. A SKILL.md is resident every turn once invoked, so its length is a per-turn cost - keep cores short and put detail inreferences/.tools/— offline-first Python.contractandbuildare whatsdrf-annotateruns (Steps 3 and 6: the column contract of a template union, and the deterministic SDRF expander fromsamples.tsv+technical.tsv);massive-files(annotate, review) andcellline lookup(annotate) are the other helpers a skill calls.check,score,fix,benchmark, andverifyare called by no skill — reachable only by hand or from CI, which smoke-tests all subcommands.massive-filesasks MassIVE's PROXI record for the dataset's FTP root and tries it first (proxi_ftp_url): a bare MSV yielded no root at all, and the ProteomeCentral route yields one with the version directory missing, so both fell through to guessing/v02/and timed out on the many datasets that live under/v01/.fetch_jsonhonours the response charset for the same ISO-8859-1 reason as the MCP client.spec/— the runtime data contract (below).
Skill dependency graph. sdrf-annotate is the hub: it calls cellline and techrefine while
annotating, ends every annotation with a fresh-context sdrf-adversarial-review (Step 9.5: manifest,
track, dispatch, repair, re-review, gate), and in review mode does what sdrf-review and
sdrf-annotate-reviewed used to. sdrf-campaign screens a class of studies and loops annotate over the included ones. contribute runs sdrf-tools review-gate gate before publishing. sdrf-tools doctor replaces setup; validate, fix, knowledge, techrefine and cellline are
references and tool commands now. Removed 2026-09-23: sdrf-review, sdrf-annotate-reviewed, sdrf-design (folded into
annotate's review mode and references/review-checks.md), sdrf-templates (now
sdrf-annotate/references/templates.md), sdrf-convert (a README block). None of this routing is covered by CI
(tools-tests.yml ignores skills/**).
Bundled paths resolve against the bundle root, not the cwd. The spec references in skills/
are still written bare (spec/sdrf-proteomics/TERMS.tsv), so every skill that reads one opens with a
Bundle paths blockquote telling the agent to resolve them against
${SDRF_SKILLS_ROOT:-${CLAUDE_PLUGIN_ROOT}} (else the sdrf-skills checkout) and to
run the helpers as sdrf-tools …. .mcp.json, hooks/hooks.json and the plugin venv commands in
setup.md stay on CLAUDE_PLUGIN_ROOT: only Claude Code reads them. hooks/check-deps.sh reads
SDRF_SKILLS_ROOT, then CLAUDE_PLUGIN_ROOT, then falls back to .. On the Python side,
resolve_terms_tsv() finds the bundled TERMS.tsv from __file__ before falling back to a
cwd-relative path. Keep both when adding a skill: installed as a plugin, the cwd is the user's
project, and a cwd-relative read silently misses (degrading to the hardcoded fallback map).
Frontmatter schema (uniform across the 14 domain skills — match it exactly)
---
name: sdrf-annotate # matches the directory (skills/sdrf-annotate/); invoked as /sdrf-skills:sdrf-annotate once installed as a marketplace plugin
description: Use when the user wants to ... Triggers on ... # always starts "Use when the user"
user-invocable: true
argument-hint: "[PXD accession or experiment description]"
---No skill uses allowed-tools, model, or version. The two review-gate skills are the exception to
the schema above: they declare only name + description and are not user-invocable — they are
dispatched by other skills into a fresh context, never typed as a slash command.
Adding/renaming a skill: the 6-file lockstep
Nothing in CI or the tests checks this, which is why the copies have already rotted. Update all of:
CLAUDE.md, README.md (badge, prose, table, tree), GEMINI.md, .opencode/AGENTS.md,
.cursor/rules/sdrf-skills.mdc, .codex/INSTALL.md. .claude-plugin/plugin.json needs no edit —
it points at the directory.
Domain policy (value encoding, reserved words, modification syntax, UNIMOD swaps, label types, row
identity) exists once, in skills/sdrf-annotate/references/format-rules.md; sdrf-annotate and
sdrf-annotate's core and references point to it. Do not paste rules back into a skill.
MCP
.mcp.json wires the bundled mcp/server.py (FastMCP, name sdrf-pride-pmc), launched via
${CLAUDE_PLUGIN_ROOT:-.}/.venv/bin/python. The entry must carry "type": "stdio": without it
Claude Code never resolves command and tries to exec a binary literally named stdio, failing with
ENOENT: no such file or directory, posix_spawn 'stdio'. It exposes 11 tools: search_projects, search_extensive,
get_project_details, get_project_files, get_article_metadata, get_pdf_by_unpaywall, search,
searchClasses, getChildren, get_full_text_article, get_full_text_section. fastmcp/httpx are in requirements.txt and
environment.yml. The server depends on .venv/ existing next to the plugin
(uv venv "$CLAUDE_PLUGIN_ROOT/.venv" && uv pip install --python "$CLAUDE_PLUGIN_ROOT/.venv/bin/python"
-r "$CLAUDE_PLUGIN_ROOT/requirements.txt") — a marketplace install ships no venv, and a plugin
upgrade replaces the directory, so it has to be re-created. Conda users repoint command.
Skills still call five tools that exist in no bundled server — searchClassesWithEmbeddingModel,
listEmbeddingModels, searchWithEmbeddingModel (in sdrf-annotate and its references, formerly sdrf-knowledge), and
search_articles / search_preprints (in sdrf-annotate). Those paths need an external OLS/PubMed/
bioRxiv MCP or a rewrite onto searchClasses/getChildren.
search_projects is one page; search_extensive is the whole sweep. The latter takes a LIST of
keywords, pages each to a short page, unions and dedupes on all_accessions, and reports what each
keyword contributed (per_keyword.new = 0 means that keyword was redundant). PRIDE ANDs the terms in
a keyword and then RANKS rather than filters, so recall — not pagination — is the binding limit: a
16-keyword single-cell sweep unions to more datasets than single-cell proteomics returns alone
(measured today: nanoPOTS 11 + proteoCHIP 11 = 22 unique, zero overlap). Exhaustion is proven
ONLY by a short page; an empty page after a full one is ambiguous, so it triggers a year-partitioned
(filter=submissionDate==YYYY) retry and anything still unresolved lands in truncated rather than
being reported as complete. PRIDE's historical bare-keyword 100-cap (#28) no longer reproduces —
verified 2026-08-19, v2 and v3 both paginate — which is why the partitioning is a detector, not an
unconditional workaround.
Article identifiers: one parser — silent-corruption class. Every article tool classifies its
input with _parse_article_id: bare forms, PMID: / PMCID: / doi: prefixes, and doi.org / PubMed /
PMC / Europe PMC URLs. Bare digits are a PMID everywhere; the full-text tools resolve a PMID or DOI to
its PMCID. Unrecognised input still yields an error record, not an exception, which a skill reads
as "no evidence" — so teach the parser a new spelling rather than adding a rule to a skill. Pinned by
tests/test_mcp_pride.py.
get_project_details / search_projects span PRIDE and MassIVE. MSV… routes to MassIVE's
PROXI API; PXD… tries PRIDE then falls back to MassIVE, because MassIVE-hosted ProteomeXchange
datasets 404 in PRIDE (PXD003626 is a live example) — a PXD prefix is not evidence of PRIDE
residency. Results carry repository and all_accessions (a dataset may hold both an MSV and a PXD;
dedupe on it). MassIVE metadata is far thinner — measured over 100 datasets each: instruments
PRIDE 100% / MassIVE 3%, experiment_types and quantification_methods never published by MassIVE
(PRIDE 100% / 14%). Screen MassIVE candidates from the publication; empty ≠ excluded.
PROXI parsing has three traps, all silent — payload is in value not name; species /
publications / contacts nest one list deeper; and a missing value is often the literal string
"null". _proxi_values() is the single place all three are handled — do not hand-roll PROXI parsing
elsewhere.
MassIVE serves charset=ISO-8859-1. httpx.Response.json() decodes raw bytes as UTF-8, so one
accented character (author names, European affiliations) raised UnicodeDecodeError and silently
dropped a whole result page — 29% recall loss on a measured query. _cached_get_json now falls back
to json.loads(resp.text), which honours the declared charset. It also takes not_found_ok=True to
map a 404 to [], because MassIVE 404s when you page past the end and that is exhaustion, not failure.
PRIDE keyword search ANDs its terms. search_projects(keyword=…) narrows hard on multi-word input
(metaproteomics → 100+, human gut metaproteomics → 2). Issue several short keywords and union the
accessions. PRIDE hits return the structured fields as plain strings, where get_project_details
returns CvParam dicts for the same logical fields; _names() handles both. On MassIVE's PROXI,
resultType is required, pageNumber is 1-based, and keywords= is silently ignored — it returns
the unfiltered listing, so only search= actually filters.
Spec data contract (spec/, read at runtime — never hardcode)
- Columns:
spec/sdrf-proteomics/TERMS.tsv - Manifest:
spec/sdrf-proteomics/sdrf-templates/templates.yaml - Templates:
spec/sdrf-proteomics/sdrf-templates/{name}/{version}/{name}.yaml
TERMS.tsv is a glossary, not a column index. 223 rows vs 345 distinct template columns, overlapping
only 204. For "is this column valid for template X?", the resolved template YAML is authoritative.
9 tab-separated columns (term/type/ontology_term_accession/usage/values/description/allow_not_available/allow_not_applicable/allow_pooled),
CRLF-terminated with one blank row — parse with
csv.DictReader(open(p, newline='', encoding='utf-8-sig'), delimiter='\t') and filter empty terms, or
allow_pooled == 'false' fails against 'false\r'. Header is type[term]
(characteristics[organism]) except type == 'anchor column' (source name, assay name,
technology type), which is the bare term — anchor column[source name] is invalid SDRF.
The values field drives ontology routing, but it is prose, not a closed grammar. Common shapes:
comma-separated prefixes in preference order (disease → MONDO, EFO, DOID, PATO) → OLS;
fixed: a, b, c → closed enum, no OLS call; pattern:/integer/numeric/free text → no OLS;
prefix + subtree restriction (MS, PRIDE (children of MS:1000044)). ~26 of 223 rows fit none of
these — do not write a parser that assumes total coverage, and never split on , without handling
the parenthetical, or you get the bogus ontology PRIDE (children of MS:1000044).
Templates: mutually_exclusive_with appears 0 times in templates.yaml — exclusivity lives in
the individual YAMLs and the graph is asymmetric, so compute the symmetric closure or you will
propose human + plants. Only ms-proteomics, affinity-proteomics, and ms-metabolomics are
usable_alone: true; base and sample-metadata are construction artifacts — never offer them.
excludes is inert: resolve_templates.py surfaces it as metadata only, and merge_columns never
consults it. The version in extends: sample-metadata@>=1.0.0 is likewise decorative — the resolver
splits on @ and always loads latest. Read spec/scripts/resolve_templates.py rather than
reimplementing merge semantics.
SDRF invariants (silent-corruption class — wrong output, no exception)
- NEVER guess ontology accessions — always verify via OLS. This is not self-enforcing: only
annotateandcelllinestate it about accessions specifically, andknowledge— despite being the "background" skill — contains no such language at all. This file is the only global home for it. - UNIMOD:1 = Acetyl, UNIMOD:21 = Phospho — the #1 swap.
tools/column_ontology_map.py'sUNIMOD_KNOWNis what detects these; every row in it was verified against OLS on 2026-09-21 (issue #73 found 8 wrong rows, includingUNIMOD:374→Propionamide, which madecheckendorse the error it exists to catch). Labels are unique, soUNIMOD_BY_NAMEinverts the table: when NT= names a known modification, the accession is treated as the typo. Verify any new row against OLS before adding it — a wrong entry here is invisible by construction. - Reserved words:
not available/not applicable— neverN/A,NA,unknown. Gated per-column by TERMS.tsv'sallow_*booleans. Exception:sdrf-campaign's screening phase emits a six-column decision TSV, not an SDRF —labelisinclude/exclude/uncertain, never a reserved word, anduncertainnever belongs in an SDRF. - Modification format:
NT=name;AC=UNIMOD:id;TA=aa;MT=Fixed|Variable. A positional value (Protein N-term) belongs inPP=, notTA=. - All ontology terms carry label + accession (e.g. "breast carcinoma" / MONDO:0007254). This file
is the correct copy —
GEMINI.md:45,.cursor/rules/sdrf-skills.mdc:44, and.opencode/AGENTS.md:50all say EFO:0000305; fix those toward this file, not the reverse. - Duplicate columns are legal and load-bearing: multiple
comment[modification parameters](one per mod) and multiplecomment[sdrf template](one per template) — bothcardinality: multiple.tools/sdrf_parser.pydisambiguates with__Nkeys; keying rows by raw name silently reads only the first occurrence. - Validate before presenting any SDRF:
parse_sdrf validate-sdrf --sdrf_file X --template Y, after refreshing the submodule. Several-t/--templateflags validate against the union of those templates (the--helpsays so andtests/test_build.pyproves it on three curated references, 2026-09-23); the older behaviour where only the last--templatecounted (observed 2026-07-17 on PXD061710) is gone from currentsdrf-pipelines. If you are on an old install, run once per declared template. For the authoritative multi-template constraint set (column licensing + reserved-wordallow_*), resolve withspec/scripts/resolve_templates.py— parse_sdrf enforces neither.parse_sdrfships insdrf-pipelinesand is not installed by default (CI installs onlyrequests,pytest,fastmcp,httpx— notsdrf-pipelines[ontology], which is heavy) — runsdrf-tools doctor(install notes:sdrf-annotate/references/setup.md). Keep concurrentparse_sdrfjobs ≤ 2. - A producer must never approve its own SDRF. For changed SDRFs, require a passing receipt from
sdrf-adversarial-review; any edit invalidates the receipt and requires a fresh reviewer. Enforced bysdrf-tools review-gate(track,pending,status,gate,approve), which discovers changed artifacts from git and binds each receipt to the artifact's SHA-256, so an approval cannot outlive the content it describes.gateexits 1 while review is pending. Enforcement lives in the CLI, not the Stop hook, because four of the five platforms this repo supports cannot run Claude Code hooks at all. - Vendor RAW only in
comment[data file]—.raw/.d/.wiff/.wiff2, never peak lists (.mgf/.mzML/.mzXML). A peak-list reference validates structurally but breaks reprocessing. - Row-uniqueness coordinate: (
source name,characteristics[biological replicate],comment[technical replicate],comment[fraction identifier]) must be unique; the spec MUST-unique key issource name+assay name+comment[label]. Thesdrf-annotated-datasetsreview gate rejects collisions, so annotate/fix/contribute must produce it. - Value encoding by column type:
characteristics[...]= the bare ontology label (neverNT=;AC=);comment[...]=NT=<OLS label>;AC=<accession>; structured characteristics (spiked compoundCT=/QY=,pooled sampleSN=) keep key-value. - Acquisition method (
comment[proteomics data acquisition method], required for MS) is a descendant ofPRIDE:0000659— DDAPRIDE:0000627, DIAPRIDE:0000450, PRMPRIDE:0000629, SRMPRIDE:0000630; neverMS:1000206/NCIT:C161786;comment[dia method]was removed. - Contribution hygiene: a
datasets/PR adds exactly one new{ACC}/folder —git diff --cached --name-statusmust show 0 deletions/modifications to other datasets; unresolved datasets go to CI-exemptsandbox/with aBLOCKED:note; no AI/assistant attribution in public commits or PRs. Never invent sample->file/channel maps, demographics, or runs.
Landmines
- CI runs no tests on the product.
tools-tests.ymlis path-filtered totools/**,tests/**,requirements.txt. A PR touching onlyskills/**— the actual product — plushooks/,mcp/,plugin.json, or docs runs zero tests.check-spec.ymlhas no path filter so it runs on every PR, but only warns when the spec is stale; it never fails the build. - Fixture path-filter gap:
tests/conftest.pyreadsexamples/PXD_synthetic.sdrf.tsvandtools/cellline_db.pydefaults todata/*.tsv, but neitherexamples/**nordata/**is in the CI filter. Editing them can break main with CI never running — run pytest locally. tests/test_services.pymocks fail open: it pre-seeds the private_cachewith a key that must byte-for-byte equalf"{base_url}{endpoint}|{params}". Any mismatch issues a live request to Cellosaurus/UniProt/EBI instead of failing. PreferMagicMock(spec=OLSClient)injection, astests/test_hallucination.pydoes.- Dead code:
tools/services.pyis imported only by its own test. tools/sdrf_fixer.pyimplements 7 fix patterns, not the 10 its own docstring and the README claim (comments number them 1,3,4,5,6,7,9).
In this repository6 other files it gives its agents.
- .opencode/AGENTS.mdAGENTS.md
- .cursor/rules/sdrf-skills.mdcCursor rule
- sdrf-adversarial-reviewSkill
- sdrf-annotateSkill
- sdrf-campaignSkill
- sdrf-contributeSkill
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

