Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 172 additions & 0 deletions .claude/skills/soulcap-cl-matching/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
---
name: soulcap-cl-matching
description: Propose a Cell Ontology (CL) match for a SOULCAP cell type (one, several, or all of them) using both marker-axiom scoring and name-based lexical search, cross-checking the two before trusting either, and writing the result into reports/candidate_cl_mappings.md with rationale and evidence. Invoke when asked to map/match a SOULCAP cell type to CL, propose a CL ID, or extend Milestone 4 candidate mappings.
---

You propose SOULCAP → Cell Ontology (CL) mappings the way they were actually
worked out by hand across many cell types: run both matching methods, never
trust either one blindly, verify anything uncertain directly against CL, and
write up the result with enough evidence that someone else can check your
reasoning. See [ROADMAP.md](../../../ROADMAP.md) Milestone 4 for the
deliverable this feeds, and [CLAUDE.md](../../../CLAUDE.md) for repo
conventions.

## When to invoke

The user asks to map/match one or more SOULCAP cell types to CL, propose a CL
ID, review/extend the candidate mappings report, or investigate why a
particular SOULCAP row has no good CL match.

## Tools

- `uv run soulcap-match --req-excl ... --req-pheno ... --parent "..."` —
single-profile marker-axiom scoring (paste the sheet's four marker columns
directly).
- `uv run soulcap-match --batch [--lexical] [--ready-only]` — scores every row
of `data/marker_combinations.csv` in one pass; `--lexical` adds an
independent OLS4 name search and writes an agreement report.
- `uv run soulcap-lookup "<term>" --ontology cl` (or the `ontology-term-lookup`
skill) — direct OLS4 verification when the batch output is ambiguous or
suspicious.
- `reports/cl_pro_relationships.md` — CL's own marker axioms, human-readable;
worth reading directly for a specific CL ID when you need to see *why* it
scored the way it did.

## Step 1 — get candidates

For one cell type, pull its four marker columns from
`data/marker_combinations.csv` (Required exclusion, Ideal exclusion, Required
phenotypic markers, Ideal phenotypic markers) and run:

```bash
uv run soulcap-match \
--req-excl "<Required exclusion>" \
--ideal-excl "<Ideal exclusion>" \
--req-pheno "<Required phenotypic markers>" \
--ideal-pheno "<Ideal phenotypic markers>" \
--parent "<Full Name>" --subset "<Abbreviation>" --top 5
```

For several/all cell types at once, use batch mode instead:

```bash
uv run soulcap-match --batch --lexical --ready-only
```

This writes `reports/candidate_cl_mappings_batch.tsv` (marker-based, top-N
per row, with `contradictions`/`disqualified` columns) and
`reports/candidate_cl_mappings_agreement.tsv` (marker top-1 vs. lexical top-1
side by side, with an `agreement` column).

## Step 2 — never trust rank 1 blindly

Both methods fail in different, uncorrelated ways — this was learned the hard
way across NK, ILC, DC, and Basophil rows:

- **Marker-based top-1 can be a real match that's too specific**, because CL
frequently has no marker axioms on the general parent class, only on its
subtypes (e.g. plain "natural killer cell" has zero marker axioms; only its
CD56-bright/CD56-dim children do). A broad, unqualified SOULCAP row (no
subset markers) usually wants the *general* CL term even if it scores worse
than a subtype.
- **Lexical top-1 can pick a wrong-species or wrong-specificity term** free-text
ranking doesn't understand biology — it has picked a *mouse*-only term over
an available human one, and an overly-specific subtype over the general term
the query text actually named. Always check the label for `, mouse` and for
extra qualifiers the SOULCAP profile doesn't test.
- **A `disqualified` or non-empty `contradictions` candidate is a hard no**
for that required marker — skip it even if it has the highest score.
- **If marker and lexical disagree, that's a signal to look closer, not
noise** — cross-check both by hand rather than picking either automatically.
- **A clean top-1 with no conflicts still isn't proof of correctness** — it
just means nothing actively contradicts it. Sanity-check the label makes
biological sense for the SOULCAP row's intent.

## Step 3 — verify anything uncertain directly against CL

When the batch output disagrees with itself, or a candidate label looks
suspicious (unexpected species, unexpected specificity, or a real match seems
to be missing from both lists), search CL directly instead of trusting either
list:

```bash
uv run soulcap-lookup "<term you actually want>" --ontology cl --rows 10
```

Or fetch a specific candidate's own axioms to see why it scored the way it
did:

```bash
uv run soulcap-lookup --id CL:XXXXXXX --ontology cl
grep -n "## CL:XXXXXXX" -A 20 reports/cl_pro_relationships.md
```

This is how several real mismatches were caught: a lexical pick for one
numbered subtype (e.g. "cDC1") turning out to be the *other* subtype's term
because free-text search didn't discriminate "1" vs "2"; a match hiding
because it needed an OR-group marker check
(`(CD193+|FceR1a+|HLA-DR-|CD303-)`) rather than a single positive marker.

## Step 4 — write the result

Append an entry to `reports/candidate_cl_mappings.md` following the existing
format (one `##` section per cell type or closely related family):

```markdown
## <Abbreviation> — <Full Name>

**SOULCAP definition:** <marker columns, briefly>

| Field | Value |
|---|---|
| Proposed CL term | <label> |
| Proposed CL ID | `CL:XXXXXXX` |
| Match type | Exact / Broad |

**Rationale:** <why this term, referencing marker matches AND/OR lexical
agreement, and calling out anything that had to be verified by hand>

**Evidence:** <marker→gene table entries, literature quotes if available from
reports/literature/, `soulcap-match` rank/score>
```

If a candidate is genuinely uncertain (e.g. the SOULCAP profile doesn't test
a marker needed to distinguish two otherwise-equal CL candidates), say so
explicitly — mark it **tentative** and name the alternatives, rather than
picking one silently. See the `ILCp` entry in `candidate_cl_mappings.md` for
the pattern.

## Step 5 — flag anything wrong in either resource

This project's aim isn't just mapping — it's finding gaps. While matching,
watch for:

- **A CL gap**: the "obviously right" CL term scores poorly only because CL
itself lacks an axiom a defining marker should have. Log it in
`reports/cl_term_issues.md` (see existing entries for the format), and ask
before filing a real GitHub issue (`gh issue create`, labelled
`cl-correction`, following the pattern of issues #12/#13/#14).
- **A SOULCAP sheet issue**: a typo in `Full Name` that breaks lexical search,
a malformed marker expression, etc. This repo cannot edit the master Google
Sheet — document it clearly (which row, what's wrong, likely fix) instead
of patching `data/marker_combinations.csv`, which is a regenerable,
gitignored cache.

## Known limitations to keep in mind

- Marker-axiom scoring only ever ranks CL terms present in
`reports/cl_pro_relationships.tsv` (826 terms as of the last sync) — a
correct answer outside that set is structurally invisible to it; lexical
search is the only way to find those.
- OR-groups (`|`-joined, e.g. `(CD193+|FceR1a+|HLA-DR-|CD303-)`) are scored as
"any one alternative satisfies," not "all required" — this was a real bug,
fixed in `extract_marker_clauses()`. A bracket with a trailing qualifier
negating a compound AND-group (`[HLA-DR+ CD11chi]-`) is **not** modelled
correctly yet — it falls back to flattening each leaf as independently
required. If you hit one of these while matching, note it rather than
trusting the score.
- Batch lexical search promotes an exact (case-insensitive) label match to
rank 1 when present in a widened pool — but if the SOULCAP `Full Name` has
a typo, lexical search will search the wrong string and can return nothing
useful. Check the sheet's `Full Name` for typos before concluding "no CL
term exists."
48 changes: 48 additions & 0 deletions .github/ISSUE_TEMPLATE/mapping_gap.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: Mapping gap
description: A SOULCAP cell type has no reasonable CL match, or CL's marker axioms conflict with SOULCAP's marker panel for a candidate match.
title: "[gap] <SOULCAP abbreviation>: <short description>"
labels: ["gap"]
body:
- type: markdown
attributes:
value: |
Use this for gaps, not proposed mappings — a case where matching
broke down, not a candidate you're confident in. Confident candidates
go in `reports/candidate_cl_mappings.md`; gaps also get logged in
`reports/gaps.tsv` (add a row there too, if practical, so this issue
has a durable, filterable record alongside it).
- type: input
id: soulcap_abbreviation
attributes:
label: SOULCAP abbreviation
placeholder: e.g. NK, ILC1, Transitional
validations:
required: true
- type: dropdown
id: gap_type
attributes:
label: Gap type
options:
- no_reasonable_cl_match — no CL term fits, even loosely
- cl_marker_conflict — a candidate CL term's asserted markers contradict SOULCAP's panel
- cl_axiom_gap — CL is missing a marker axiom or term a defining SOULCAP marker implies
- sheet_data_issue — the SOULCAP sheet itself has missing/incorrect data blocking a match
validations:
required: true
- type: textarea
id: description
attributes:
label: Description
description: What was tried, what broke down, and why. Link the SOULCAP row and any candidate CL IDs considered and rejected.
validations:
required: true
- type: input
id: related_cl_ids
attributes:
label: Related CL ID(s), if any
placeholder: "CL:0000623"
- type: textarea
id: evidence
attributes:
label: Evidence
description: Links to reports/, marker data, or literature backing this up.
50 changes: 50 additions & 0 deletions .github/workflows/robot-qc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: ROBOT ontology QC

on:
pull_request:

jobs:
# Standalone, read-only audit of upstream CL — never merges anything from
# this repo into it, never asserts our candidate mappings as true. Catches
# pre-existing CL bugs (e.g. duplicate equivalence/subclass axioms,
# unsatisfiable classes) worth reporting upstream. Our own candidate
# mappings stay as SSSOM (reports/candidate_cl_mappings.sssom.tsv) — a
# proposal for human review, not something this repo should unilaterally
# convert into OWL axioms and reason over as if already accepted.
robot-upstream-cl:
name: ROBOT report + reason — upstream CL
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Java
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "21"

- name: Install ROBOT
run: |
curl -sL -o robot.jar https://github.com/ontodev/robot/releases/latest/download/robot.jar
curl -sL -o robot https://raw.githubusercontent.com/ontodev/robot/master/bin/robot
chmod +x robot
echo "$PWD" >> "$GITHUB_PATH"

- name: Download CL (base, import-free release artifact)
run: curl -sL -o cl-base.owl http://purl.obolibrary.org/obo/cl/cl-base.owl

- name: ROBOT report — structural issues (duplicate axioms etc.)
run: ./robot report --input cl-base.owl --fail-on ERROR --output cl-base-report.tsv

- name: ROBOT reason (ELK) — unsatisfiable classes / bad entailments
run: ./robot reason --input cl-base.owl --reasoner ELK --output cl-base-reasoned.owl

- name: Upload upstream QC artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: robot-upstream-cl-qc
path: |
cl-base-report.tsv
cl-base-reasoned.owl
if-no-files-found: ignore
18 changes: 15 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ See [README.md](README.md) for the full overview and setup.
| [ROADMAP.md](ROADMAP.md) | Planned work / milestones and their deliverables and dependencies. |
| [Notes.md](Notes.md) | Scratch notes on data sources + a marker-syntax quick reference. |
| [MARKER_SYNTAX.md](MARKER_SYNTAX.md) | **Canonical spec** of the marker expression language (human-readable guide + EBNF). Cite this for anything parsing/validating marker strings. |
| [reports/](reports/) | Analysis reports. `marker_string_issues.md` is the curated review; `marker_validation.md` is **auto-generated** by the EBNF validator on each sync. `cl_pro_relationships.md`/`.tsv` are **auto-generated** by `soulcap-cl-pro` from Ubergraph. New reports go here. `reports/citation_traversal/` holds gitignored, regenerable snippet caches + summaries from the `citation-traversal` skill. |
| `src/soulcap_cl_mapping/` | All Python code. `sync_sheets.py` → `soulcap-sync`; `marker_syntax.py` → `soulcap-validate` (EBNF validator); `snippet_cache.py` → `soulcap-cache`; `report_validator.py` → `soulcap-validate-report`; `cl_pro.py` → `soulcap-cl-pro` (CL→PR relationships via Ubergraph SPARQL). |
| [reports/](reports/) | Analysis reports. `marker_string_issues.md` is the curated review; `marker_validation.md` is **auto-generated** by the EBNF validator on each sync. `cl_pro_relationships.md`/`.tsv` are **auto-generated** by `soulcap-cl-pro` from Ubergraph. `cl_term_issues.md` tracks proposed CL corrections found during mapping work. `candidate_cl_mappings.md` is the Milestone 4 deliverable — proposed SOULCAP→CL mappings with rationale and evidence. `candidate_cl_mappings_batch.tsv`/`candidate_cl_mappings_agreement.tsv` are **auto-generated** by `soulcap-match --batch [--lexical]` (issue #6) — a marker-axiom-only pass and a marker/lexical agreement pass across every SOULCAP cell type; treat as an unreviewed draft shortlist (conflicts must be checked), not curated output. `candidate_cl_mappings.sssom.tsv` is **auto-generated** by `soulcap-sssom` from the curated `CURATED_MAPPINGS` table in `sssom_export.py` (kept in sync by hand with `candidate_cl_mappings.md`) — standard SSSOM format, with `confidence`/`comment` derived from whether the match is backed by a directly-asserted CL marker axiom, an inferred-only one, or none at all (lexical/name match only). `gaps.tsv` is a curated, hand-maintained log of cases where SOULCAP has no reasonable CL match, CL conflicts with a marker panel, CL has an axiom gap, or the sheet itself has a data problem — separate from the mapping table; matching GitHub issues use the `gap` label (`.github/ISSUE_TEMPLATE/mapping_gap.yml`). New reports go here. `reports/citation_traversal/` holds gitignored, regenerable snippet caches + summaries from the `citation-traversal` skill. |
| `src/soulcap_cl_mapping/` | All Python code. `sync_sheets.py` → `soulcap-sync`; `marker_syntax.py` → `soulcap-validate` (EBNF validator); `snippet_cache.py` → `soulcap-cache`; `report_validator.py` → `soulcap-validate-report`; `cl_pro.py` → `soulcap-cl-pro` (CL→PR relationships via Ubergraph SPARQL); `cl_match.py` → `soulcap-match` (marker-axiom + lexical CL candidate scoring, single-profile or `--batch`); `oak_match.py` → `soulcap-oak-match` (OAK sqlite-backed lexical/synonym CL search — downloads/caches a local CL database on first use, ~100MB); `sssom_export.py` → `soulcap-sssom` (curated mappings → SSSOM TSV, with confidence derived from CL marker-axiom assertion status). |
| `tests/` | Unit tests (mirror `src/` layout). |
| `.claude/skills/` | Project skills — see [citation-traversal](.claude/skills/citation-traversal/SKILL.md) and `ontology-term-lookup`. |
| `.claude/hooks/` | Claude Code hooks. `validate_report_quotes.py` is a PreToolUse guard that blocks writing a citation-traversal report whose quotes aren't verbatim in the snippet cache. |
Expand All @@ -64,7 +64,13 @@ documents at the repo root.
- **All code lives under `src/`** (package `soulcap_cl_mapping`).
- **All code must have ≥80% unit-test coverage**, with tests under `tests/`
using a standard harness (`pytest`).
- **GitHub Actions must run the tests on all PRs.**
- **GitHub Actions must run the tests on all PRs.** `.github/workflows/tests.yml`
runs lint/format/mypy/pytest. `.github/workflows/robot-qc.yml` runs a
read-only ROBOT audit of upstream CL (`cl-base.owl`, the import-free release
artifact) with `robot report` + `robot reason` (ELK), independent of our
mappings. It never merges our own candidate mappings into CL or reasons
over them as if accepted — those stay as SSSOM (`soulcap-sssom`), a
proposal for human review, not something this repo asserts unilaterally.
- Use **UV** for environment and dependency management (`uv sync`,
`uv run ...`). Don't invoke `pip` directly.
- The marker expression language is precisely defined in
Expand Down Expand Up @@ -106,6 +112,12 @@ exact ontology labels via OLS4 — prefer it for term resolution.
any quote isn't verbatim in the cache. See
[SKILL.md](.claude/skills/citation-traversal/SKILL.md).
- **`ontology-term-lookup`** — resolve biological terms to ontology labels via OLS4.
- **`soulcap-cl-matching`** — propose a CL match for a SOULCAP cell type using
both marker-axiom scoring (`soulcap-match`) and lexical search, cross-checked
against each other and verified via direct OLS4 lookup when uncertain, then
written into `reports/candidate_cl_mappings.md` with rationale and evidence.
The Milestone 4 workflow. See
[SKILL.md](.claude/skills/soulcap-cl-matching/SKILL.md).

> Local RAG indexing (a `local-paper-index` skill) was trialled here but removed
> to avoid confusion — it was copied verbatim from `atlas_chat` and unused.
Expand Down
Loading
Loading