diff --git a/.claude/hooks/validate_report_quotes.py b/.claude/hooks/validate_report_quotes.py index de3c276..55fb752 100755 --- a/.claude/hooks/validate_report_quotes.py +++ b/.claude/hooks/validate_report_quotes.py @@ -86,7 +86,9 @@ def main() -> int: try: from soulcap_cl_mapping import report_validator except Exception as exc: # pragma: no cover - defensive - print(f"validate_report_quotes: import failed ({exc}); allowing", file=sys.stderr) + print( + f"validate_report_quotes: import failed ({exc}); allowing", file=sys.stderr + ) return 0 run_id = path.parent.name @@ -104,7 +106,10 @@ def main() -> int: evidence = snippet_cache.load_snippet_texts(run_id, qid, None) except Exception as exc: # pragma: no cover - defensive - print(f"validate_report_quotes: cache read failed ({exc}); allowing", file=sys.stderr) + print( + f"validate_report_quotes: cache read failed ({exc}); allowing", + file=sys.stderr, + ) return 0 if not evidence: diff --git a/.claude/skills/gap-issue-filing/SKILL.md b/.claude/skills/gap-issue-filing/SKILL.md new file mode 100644 index 0000000..e56a65b --- /dev/null +++ b/.claude/skills/gap-issue-filing/SKILL.md @@ -0,0 +1,115 @@ +--- +name: gap-issue-filing +description: Turn unfiled rows in reports/gaps.tsv into GitHub issues using the mapping_gap template, without duplicating existing issues, then update the row's status/repo_issue columns. Invoke when asked to file logged mapping gaps as issues, clear the gaps.tsv backlog, or check which gaps still need one. +--- + +You take rows already logged in `reports/gaps.tsv` — cases where matching a +SOULCAP cell type to CL broke down — and turn the ones that aren't tracked on +GitHub yet into real issues, using the existing `mapping_gap` template so they +stay consistent with issues #12–#14. See [CLAUDE.md](../../../CLAUDE.md) for +repo conventions and [ROADMAP.md](../../../ROADMAP.md) for how gaps relate to +the CL term corrections table. + +`gaps.tsv` is the durable record; GitHub issues are the actionable, triageable +copy. This skill keeps the two in sync — it never replaces `gaps.tsv` as the +source of truth. + +## When to invoke + +The user asks to file gaps as issues, clear the `gaps.tsv` backlog, or check +how many logged gaps still lack a tracking issue. + +## Tools + +- `reports/gaps.tsv` — the source rows. Columns: `soulcap_abbreviation`, + `soulcap_full_name`, `gap_type`, `description`, `related_cl_ids`, + `evidence`, `status` (`open` / `filed_as_issue`), `repo_issue`, + `date_logged`. +- `.github/ISSUE_TEMPLATE/mapping_gap.yml` — the template whose fields map + directly onto `gaps.tsv` columns: `soulcap_abbreviation` → title, `gap_type` + → the dropdown, `description` + `related_cl_ids` + `evidence` → the + matching body fields. +- `gh issue list` / `gh issue create` — via Bash. This repo is + `Cellular-Semantics/SOULCAP_CL_Mapping` (confirm with `gh repo view` if the + remote is ever ambiguous). +- `Edit` — to update `gaps.tsv`'s `status`/`repo_issue` columns once an issue + is filed. + +## Step 1 — find what's actually unfiled + +Read `reports/gaps.tsv` and list every row where `status` is `open` (not +`filed_as_issue`). Note two rows sharing a `soulcap_abbreviation` group (e.g. +`NK1 / NK3`) file as **one** issue, matching how the row itself is structured +— don't split a single row into multiple issues. + +## Step 2 — check for an existing issue first + +Before filing, search for a plausible duplicate: + +```bash +gh issue list --search " in:title" --state all +gh issue list --label gap --state all +``` + +Some gaps already have a related, non-`gap`-labelled issue (e.g. the +monocyte/B-cell axiom-gap rows reference issue #12 in their `description` even +though #12 itself is filed under a different title). Read the row's +`description` and `evidence` columns for an existing issue number before +assuming none exists — filing a duplicate is worse than leaving a row open one +more day. + +## Step 3 — confirm the batch with the user before filing + +List the rows you intend to file (abbreviation + one-line description) and get +explicit confirmation before running `gh issue create` — this creates public, +visible state (issues, notifications to watchers) that's easy to create but +tedious to clean up if wrong. Do not file silently in a loop. + +## Step 4 — file each issue + +For each confirmed row, create the issue from the template's own field +structure — reproduce it as a plain markdown body (the `gh issue create +--template` flag targets local `.github/` templates inconsistently across `gh` +versions; writing the body directly from the YAML fields is more reliable): + +```bash +gh issue create \ + --title "[gap] : " \ + --label gap \ + --body "$(cat <<'EOF' +**SOULCAP abbreviation:** () + +**Gap type:** + +**Description:** + + +**Related CL ID(s):** + +**Evidence:** + +EOF +)" +``` + +## Step 5 — write the result back to gaps.tsv + +For each filed issue, edit its `gaps.tsv` row: + +- `status`: `open` → `filed_as_issue` +- `repo_issue`: `(not yet filed)` → the issue URL `gh issue create` printed + +Do this immediately after each successful `gh issue create` — don't batch it +to the end, so a failure partway through the batch doesn't leave filed issues +unrecorded. + +## Known limitations + +- This only closes the loop one direction (gap → issue). It does not detect + when a filed issue has since been closed/resolved upstream — that's + `roadmap-status-sync`'s job for the CL term corrections table, and isn't yet + extended to `gaps.tsv` rows with `status=filed_as_issue`. +- A `sheet_data_issue` row (a typo/malformed string in the master Google + Sheet) still can't be *fixed* here — filing the issue documents it, but the + actual correction still has to happen in the Sheet per + [CLAUDE.md](../../../CLAUDE.md)'s data-flow rules. diff --git a/.claude/skills/mapping-audit/SKILL.md b/.claude/skills/mapping-audit/SKILL.md new file mode 100644 index 0000000..7bb070b --- /dev/null +++ b/.claude/skills/mapping-audit/SKILL.md @@ -0,0 +1,138 @@ +--- +name: mapping-audit +description: Cross-check curated SOULCAP→CL mappings against current CL marker axioms and literature evidence to catch drift, unsupported confidence, or mistyped broad/exact matches — the Milestone 3 deliverable, run today as a drift/evidence audit even though the Sheet hasn't yet gained the broad/exact + comment columns the milestone originally specified. Invoke when asked to audit curated CL mappings, check Milestone 3 progress, or re-verify candidate_cl_mappings.sssom.tsv after a CL or SOULCAP sheet update. +--- + +You re-verify mappings that were already curated in +[reports/candidate_cl_mappings.md](../../../reports/candidate_cl_mappings.md) +and exported to +[reports/candidate_cl_mappings.sssom.tsv](../../../reports/candidate_cl_mappings.sssom.tsv), +checking whether the evidence that justified each one still holds. See +[ROADMAP.md](../../../ROADMAP.md) Milestone 3, which this implements in +reduced form: the milestone was scoped assuming the Google Sheet would gain +dedicated broad/exact-match and comment columns first, blocking it entirely. +Those columns would sharpen *curator intent* capture, but the audit itself — +"does the evidence for this mapping still check out?" — doesn't strictly need +them. Say explicitly, in whatever you produce, that this is the +evidence/drift half of M3, not the full milestone as originally scoped. + +## When to invoke + +The user asks to audit curated CL mappings, wants a Milestone 3 progress +check, or just re-ran `soulcap-cl-pro` (CL's marker axioms can change between +Ubergraph syncs) or `soulcap-sync` (the SOULCAP sheet can change too) and +wants to know if any existing mapping is now stale. + +## Tools + +- `src/soulcap_cl_mapping/sssom_export.py`'s `CURATED_MAPPINGS` table — the + actual curated entries, each with `abbreviation`, `cl_id`, `cl_label`, + `match_type` (`Exact`/`Broad`), optional `evidence_override`, `uncertain`, + and `note`. This is the ground truth for *what was decided*; the generated + `.sssom.tsv` is its rendered output. +- `sssom_export.load_assertion_status()` / `classify_evidence()` — classify a + CL ID's current marker-axiom support as `confirmed` (directly asserted), + `inferred_only`, or `no_marker_axiom`, from + `reports/cl_pro_relationships.tsv`. +- `reports/literature/*.md` and `reports/pro_marker_species_support.tsv` — the + literature side of the evidence a mapping might rely on, especially for + species-specific marker claims. +- `uv run soulcap-match --req-excl ... --ideal-excl ... --req-pheno ... + --ideal-pheno ... --parent "..."` — re-run a SOULCAP profile's marker-axiom + scoring against the *current* CL data to see if the picture has changed + since the mapping was curated. +- `uv run soulcap-lookup --id CL:XXXXXXX` — confirm a term hasn't been + renamed, deprecated, or replaced since curation. + +## Step 1 — recompute evidence classification for every curated entry + +```bash +uv run python -c " +from soulcap_cl_mapping.sssom_export import CURATED_MAPPINGS, load_assertion_status, classify_evidence +status = load_assertion_status() +for e in CURATED_MAPPINGS: + evidence = e.get('evidence_override') or classify_evidence(e['cl_id'], status) + print(e['abbreviation'], e['cl_id'], e['match_type'], evidence, e.get('uncertain', False)) +" +``` + +This reproduces exactly what `soulcap-sssom` computes today. The audit's job +is to question it, not just print it — for each row, ask: + +1. **Does `match_type` still make sense given `evidence`?** An `Exact` match + resting on `no_marker_axiom` (lexical-only) is structurally weaker than its + label implies — flag it, even though `sssom_export.py` already discounts + its numeric confidence (0.55 vs. 0.9 for a confirmed Exact match). The + *label* still reads as "Exact" to anyone skimming `candidate_cl_mappings.md`. +2. **Has the CL side moved?** Re-fetch the CL ID with `soulcap-lookup --id` — + check the label and definition still match what `candidate_cl_mappings.md` + describes. CL is actively maintained (see issues #12–#14, all filed against + terms this project maps to); a maintainer accepting one of those upstream + fixes changes the term's markers/definition/label out from under a mapping + made before the fix landed. +3. **Does literature actually back a species-specific claim the mapping + relies on?** Cross-check against `reports/pro_marker_species_support.tsv` + — if a mapping leans on a marker that TSV records as + `insufficient_evidence` or a different species than SOULCAP's row assumes, + that's a real finding, not noise. +4. **Was it marked `uncertain` for a reason that's since been resolved (or + gotten worse)?** Re-read the `note` field and check whether the ambiguity + it describes is still accurate. + +## Step 2 — re-score against current marker axioms for anything suspicious + +When Step 1 flags a row, re-run the actual profile (pull the four marker +columns for that `abbreviation` from `data/marker_combinations.csv`) through +`soulcap-match` and compare the result to what's recorded in +`candidate_cl_mappings.md`. A new `contradictions` entry or a `disqualified` +result that wasn't there before is the clearest possible signal something +changed. + +## Step 3 — write findings, don't silently fix + +This audit's output is a report, not an automatic rewrite of curated mappings +— changing a curated match_type or CL ID is `soulcap-cl-matching`'s job (or a +human decision), not this skill's. Write findings to a new +`reports/mapping_audit.md`, one entry per flagged mapping: + +```markdown +## — → `CL:XXXXXXX` + +**Curated as:** , evidence=, confidence= + +**Finding:** + +**Suggested action:** +``` + +If this is the first run, add `reports/mapping_audit.md` to +[CLAUDE.md](../../../CLAUDE.md)'s repository layout table and to +[ROADMAP.md](../../../ROADMAP.md) Milestone 3 as its (partial) deliverable — +new artifacts get linked from both, per repo convention. + +## Step 4 — flag anything that's actually a full M3 blocker + +If a finding can't be resolved without the sheet's still-missing broad/exact + +comment columns (e.g. a curator's original *intent* is ambiguous and the +mapping's own `note` doesn't disambiguate it), don't force an answer — log it +as exactly the kind of case that's still blocked, so the roadmap accurately +shows partial progress plus a real remaining blocker rather than looking +either fully done or fully stuck. + +## Known limitations + +- This audits `CURATED_MAPPINGS` (the signed-off subset, currently 87 of 127 + SOULCAP rows) — it says nothing about rows that don't have a curated + mapping yet at all. That's Milestone 4's remaining work, not this skill's. +- `classify_evidence()` only sees `reports/cl_pro_relationships.tsv`, which is + itself a snapshot from the last `soulcap-cl-pro` run against Ubergraph — run + that first if CL upstream has plausibly changed (e.g. one of issues #12–#14 + was recently merged) before trusting a "nothing changed" result. +- This is genuinely a reduced version of Milestone 3 as scoped in + `ROADMAP.md` — it catches drift and re-validates evidence, but it cannot + check curator intent (was this *meant* to be broad or exact?) the way the + sheet's planned dedicated columns would. Don't report a clean audit run as + "Milestone 3 done." diff --git a/.claude/skills/pro-marker-species-support/SKILL.md b/.claude/skills/pro-marker-species-support/SKILL.md new file mode 100644 index 0000000..ce4805b --- /dev/null +++ b/.claude/skills/pro-marker-species-support/SKILL.md @@ -0,0 +1,151 @@ +--- +name: pro-marker-species-support +description: Check whether a CL PRO marker asserted on a cell type is actually validated in the literature as human, mouse, or both, and record the verdict in reports/pro_marker_species_support.tsv with a verbatim-quoted source. Invoke when asked to find literature support (or the lack of it) for the species-specificity of CL's general PRO markers, or to extend/refresh reports/pro_marker_species_support.tsv (issue #11). +--- + +You check whether a CL PRO marker that's asserted on a cell type *without* +stating a species applies to human, mouse, or both — grounded in real, +verbatim-quoted literature, never inferred from the marker name alone. This +is issue #11's workflow, proven across all 77 in-scope pairs in one run: 59 +resolved to `human` (mostly by reusing already-vetted M2 literature quotes), +18 honestly recorded as `insufficient_evidence` rather than forced. See +[CLAUDE.md](../../../CLAUDE.md) for repo conventions and +[reports/pro_marker_species_support.tsv](../../../reports/pro_marker_species_support.tsv) +for the current output. + +## When to invoke + +The user asks to find literature support for a CL PRO marker's species +applicability, work through issue #11, or extend/refresh +`reports/pro_marker_species_support.tsv` after new SOULCAP rows or CL marker +axioms are added. + +## Tools + +- `soulcap_cl_mapping.pro_species_support.get_scoped_pairs(tsv_path, curated_cl_ids)` + / `group_by_cell(pairs)` — scopes the question to (CL cell type, PRO marker) + pairs that actually matter: `asserted=True` rows in + `reports/cl_pro_relationships.tsv`, restricted to the CL IDs in + `CURATED_MAPPINGS` (`sssom_export.py`), excluding markers whose `pr_label` + already carries a species qualifier (`(mouse)`/`(human)`). Run via + `uv run python -c "..."` or a short driver script — not a CLI (deliberately; + see the module docstring). +- `reports/literature/*.md` — the Milestone 2 literature reports. **Check + these first, always.** They already contain verbatim-quoted, hook-validated + evidence for 7 cell families (NK, ILC, DC, iNKT, T cell, B cell, gdT/MAIT), + and reusing a quote from here is strictly better than a fresh search: it's + already vetted, and it's free. +- `data/citation_mgr.csv` (synced Citation Mgr tab) — per-family seed papers + for families without an M2 report yet (as of this run: basophil, + granulocyte, mast cell, monocyte, neutrophil, MDSC). +- The `citation-traversal` skill — use when the Asta MCP server is reachable + and a family has no M2 coverage. Seed round 1 from `data/citation_mgr.csv`. +- `soulcap_cl_mapping.europepmc_search.search(query, page_size=...)` / + `uv run soulcap-europepmc ""` — free, keyless fallback when Asta + isn't reachable (this was the case for the whole first run — check + `.mcp.json`/`/mcp` before assuming Asta is down). Single-round only, no + citation-graph following. Prefer a targeted query naming an already-known + canonical paper (e.g. by PMID via `EXT_ID: AND SRC:MED`) over blind + free-text search — free-text search on general marker+cell-type queries + reliably surfaces recent tangential disease-cohort papers, not clean + definitional statements. If this repo already cites a canonical paper for + the cell family elsewhere (e.g. `reports/cl_term_issues.md`, + `reports/gaps.tsv`), fetch that paper by PMID first. + +## Step 1 — scope the pairs + +```bash +uv run python -c " +from soulcap_cl_mapping.pro_species_support import get_scoped_pairs, group_by_cell, DEFAULT_TSV +from soulcap_cl_mapping.sssom_export import CURATED_MAPPINGS +ids = {m['cl_id'] for m in CURATED_MAPPINGS} +pairs = get_scoped_pairs(DEFAULT_TSV, ids) +print(len(pairs), 'pairs across', len(group_by_cell(pairs)), 'CL cell types') +" +``` + +Diff this against the CL IDs/PR markers already in +`reports/pro_marker_species_support.tsv` to find only the *new* pairs when +refreshing rather than re-running everything. + +## Step 2 — find evidence, in priority order + +For each pair (or each CL cell type's group of pairs, sharing a seed set): + +1. **Search the relevant `reports/literature/*.md` file** for the marker's + CD synonym or PR label. If a verbatim blockquote already covers it, reuse + that quote and citation directly — `evidence_source: m2_literature_report`. +2. **If Asta is reachable and no M2 coverage exists**, run `citation-traversal` + seeded from `data/citation_mgr.csv`'s row for that family — + `evidence_source: citation_traversal`. +3. **If Asta isn't reachable**, use `soulcap-europepmc` — prefer fetching a + specific known-good paper by PMID over free-text search (see Tools above) + — `evidence_source: europepmc_search`. +4. **If nothing turns up via any route**, record + `species_support: insufficient_evidence`, `evidence_source: none`, and + write a one-line note in `source_ref` naming exactly what was tried (which + file was checked, which query was run) so the gap is reproducible, not + just asserted. This is a real, useful finding — the monocyte subset panel + (CD192/CD19/CD3/CD20/CX3CR1 across all three monocyte types) came back + this way in the first run, which is itself worth knowing. + +## Step 3 — verify every quote before writing + +Never write a quote you haven't confirmed is a **literal substring** of its +source — the M2 markdown file's raw text, or the freshly fetched Europe PMC +abstract (`europepmc_search.search()` returns the `abstract` field; re-fetch +it, don't rely on a paraphrase from earlier in the conversation). Do this +with a small check before writing the row, e.g.: + +```python +assert quote in source_text, "not verbatim" +``` + +Watch for smart quotes (`"…"` vs `"…"`), en/em-dashes vs hyphens, and +markdown line-wrapping (`"...marked\n> by CLEC9A"` in the source should +collapse to one line — `.replace("\n> ", " ")` — in the TSV field, not be +written with an embedded newline, which breaks simple line-based TSV +tooling even though it's technically valid CSV-quoted). + +There is no PreToolUse hook enforcing this for this report (unlike +`citation-traversal`'s `validate_report_quotes.py`, which is scoped to +`reports/citation_traversal/*/report.md` only) — the verification is your +own discipline here. + +## Step 4 — write the row + +Append to `reports/pro_marker_species_support.tsv` +(tab-separated, one row per pair): + +| Column | Meaning | +|---|---| +| `cl_id`, `cl_label`, `pr_id`, `pr_label`, `cd_synonym` | From `get_scoped_pairs()` — don't hand-retype these, pull them programmatically to avoid transcription errors (a real bug caught during the first run: hand-typed PR labels didn't match `cl_pro_relationships.tsv`). | +| `species_support` | `human` / `mouse` / `both` / `insufficient_evidence`. | +| `evidence_source` | `m2_literature_report` / `citation_traversal` / `europepmc_search` / `none`. | +| `citation` | Author/year/PMID/DOI, plus a trailing `-- ` caveat clause when the quote's context doesn't exactly match the pair's cell type/subset (e.g. a naive-T marker's only evidence coming from a Treg paper) — say so rather than let the row imply a cleaner match than it is. | +| `quote` | The verbatim substring itself. | +| `source_ref` | `reports/literature/.md` for M2 reuse, an EuropePMC article URL for a fresh search, or `reports/citation_traversal//` for a traversal — or, for `insufficient_evidence`, a one-line note on what was tried. | + +## Step 5 — flag real findings + +If a `species_support` result is genuinely surprising (e.g. a marker CL +implicitly treats as pan-species turns out to only have mouse evidence, or a +whole marker cluster comes back `insufficient_evidence` the way monocyte +subset markers did), log it the same way other literature/CL gaps from this +project are handled: `reports/gaps.tsv` / `reports/cl_term_issues.md`, +following the existing entry format — not a new mechanism. + +## Known limitations + +- Europe PMC search is single-round, no citation-graph following — it will + miss evidence that only exists in a paper the seed doesn't directly cite. + Prefer M2 reuse or `citation-traversal` (when Asta is up) first. +- The scoping in Step 1 only covers `asserted=True` rows already in + `reports/cl_pro_relationships.tsv`, restricted to CL IDs SOULCAP already + maps to. A correct answer for a CL term outside `CURATED_MAPPINGS`, or a + marker CL hasn't asserted at all yet, is structurally out of scope here — + that's a `cl_term_issues.md`/`gaps.tsv` matter, not this skill's job. +- `insufficient_evidence` means "not found via the routes tried," not "does + not exist" — a paywalled paper or a full-text-only mention (abstract + didn't have it) can still produce a false negative. Note what was tried so + a future pass with different access can pick up where this left off. diff --git a/.claude/skills/roadmap-status-sync/SKILL.md b/.claude/skills/roadmap-status-sync/SKILL.md new file mode 100644 index 0000000..7b7a949 --- /dev/null +++ b/.claude/skills/roadmap-status-sync/SKILL.md @@ -0,0 +1,79 @@ +--- +name: roadmap-status-sync +description: Reconcile ROADMAP.md's milestone status markers (⬜/🟡/✅/⛔) against what's actually committed in the repo, and rewrite whichever are stale. Invoke when asked to update/check the roadmap's status, before reporting project progress to someone outside the repo, or whenever a milestone's deliverable changes. +--- + +You check whether [ROADMAP.md](../../../ROADMAP.md)'s status markers still +match reality, and fix them when they don't. This exists because they went +stale once already: Milestones 1, 2, and 4 sat marked ⬜ "not started" for +weeks after real deliverables had already landed, and nobody caught it until +an unrelated audit. Don't let that repeat — the roadmap is what gets shown to +people outside this repo (advisors, collaborators); it should never undersell +or oversell progress. + +## When to invoke + +The user asks to check or update `ROADMAP.md`'s status, wants an accurate +progress summary before a meeting/report, or just finished work that plausibly +changes a milestone's completion state. + +## Tools + +- `ROADMAP.md` — the file being audited. Status legend: ⬜ not started · + 🟡 in progress · ✅ done · ⛔ blocked. +- `uv run soulcap-sync` — re-pull the Google Sheet before checking anything + that depends on its current column set (this is a network call and + overwrites the gitignored `data/` cache — safe, but mention you're doing it). +- File/row counts under `marker_mappings/`, `reports/`, `reports/literature/`. +- `gh issue view ` — for the CL term corrections table, whose "Status" + column tracks upstream `obophenotype/cell-ontology` issues that this repo + doesn't control the resolution of. + +## Step 1 — recompute each milestone's real status + +Don't infer from memory or from the current emoji — recompute from what's on +disk, every time: + +| Milestone | Evidence to check | How to read it | +|---|---|---| +| M1 — marker → protein → gene | `marker_mappings/marker_protein_gene.csv` exists and has rows | Empty/missing → ⬜. Has rows but visibly incomplete (spot-check a few marker tokens from `data/marker_combinations.csv` against it) → 🟡. Covers essentially all distinct tokens → ✅. | +| M2 — literature support | Count `reports/literature/*.md` files against the distinct `Category` values in `data/citation_mgr.csv` (re-sync first if `data/` looks stale) | 0 files → ⬜. Some but not all categories covered → 🟡 (state the fraction, e.g. "8/14"). All categories covered → ✅. | +| M3 — audit curated mappings | Whether the Google Sheet now has dedicated broad/exact-match and comment columns beyond today's `Type of Match` (assay-type, not CL-match-type), `OLS CL identifier`, and `CL Mapping Notes` — check the freshly-synced sheet's actual column headers, don't assume from the column *names* alone (`Type of Match` sounds relevant but is not the same field per the roadmap's own note) | Columns still absent → stays ⛔, but if the `mapping-audit` skill has since made a drift/evidence-based audit possible without those columns, say so explicitly rather than leaving it a flat "blocked with nothing happening." | +| M4 — candidate mappings | Row count in `reports/candidate_cl_mappings.sssom.tsv` (minus header) against total rows in `data/marker_combinations.csv` (minus header) | Partial coverage → 🟡 with the fraction. All SOULCAP rows covered (including documented "no reasonable match" cases logged in `gaps.tsv`, which count as resolved, not missing) → ✅. | +| CL term corrections table | `gh issue view ` for each row's linked repo issue, and the upstream `obophenotype/cell-ontology` issue if linked | If the upstream issue is closed/merged, the table's "Status" cell is stale — update it. | + +## Step 2 — diff against what the file currently says + +Read the current milestone headers (`## Milestone N — ... `) and the CL +term corrections table's Status column. List every mismatch between Step 1's +computed status and what's written, with the evidence for each. + +## Step 3 — confirm before rewriting anything non-trivial + +A pure emoji swap backed by unambiguous evidence (e.g. a deliverable file that +plainly didn't exist before now does) can be applied directly. Anything +judgment-based — 🟡 vs ✅ on a milestone that's "mostly" done, or whether a +partially-blocked milestone should flip to 🟡 — should be confirmed with the +user first, since this file represents the project to people outside it. + +## Step 4 — write the update + +Edit the milestone heading's emoji and, where useful, add a short parenthetical +with the evidence (e.g. `## Milestone 2 — Literature support for markers 🟡 +(8/14 categories)`) so the fraction doesn't silently go stale again the same +way the flat emoji did. Update the CL term corrections table's Status cell the +same way when an upstream issue has moved. + +## Known limitations + +- This only checks the four numbered milestones and the CL term corrections + table — it doesn't audit the "Cross-cutting dependencies" section, which is + qualitative by nature. +- M2's category list comes from `data/citation_mgr.csv`'s `Category` column, + which mixes true cell-type families (NK cell, B cell, monocyte, ...) with at + least one cross-cutting method category (`immunoprofiling`) that doesn't map + to a single `reports/literature/*.md` file — don't count it against M2's + denominator; use judgment on any other ambiguous category the same way. +- A "done" `data/` count is only as fresh as the last `uv run soulcap-sync` — + always re-sync before trusting M2/M3/M4 numbers if the last sync date is + unclear or more than a few days old. diff --git a/.claude/skills/soulcap-cl-matching/SKILL.md b/.claude/skills/soulcap-cl-matching/SKILL.md new file mode 100644 index 0000000..c6b819b --- /dev/null +++ b/.claude/skills/soulcap-cl-matching/SKILL.md @@ -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 "" --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 "" \ + --ideal-excl "" \ + --req-pheno "" \ + --ideal-pheno "" \ + --parent "" --subset "" --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 "" --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 +## — + +**SOULCAP definition:** + +| Field | Value | +|---|---| +| Proposed CL term |