Repository navigation
feat(api): cohort filters can exclude or isolate analysis results - #67
Merged
Merged
Conversation
Analysis result series carry the collection_id of the images they
analyze, so a collection_id filter returns both, and no term value
selects original data: its analysis_result_id is NULL. Through the
cohort surfaces there was no way to drop the derived series (two thirds
of NLST) short of rewriting the query in SQL and losing the manifest.
CohortFilters gains analysis_results ('include' / 'exclude' / 'only'),
named and valued as in idc-index's download_from_selection so the two
tools read the same. It compiles to analysis_result_id IS [NOT] NULL on
top of terms and ranges. It is not a predicate on its own for
require_filter, since either side is about half of IDC, and 'exclude'
with an analysis_result_id term is refused instead of returning a zero
that reads as "no such data". For a collection-only filter the download
payload switches from `idc download <collection>`, which would fetch the
analysis results too, to `idc download-from-selection --analysis-results`.
The issue preferred a source_type term. A mode field matches idc-index,
needs no synthetic column, and keeps the choice visible in
filters_applied.
get_attribute_values also returns null_count. Previously it listed the
analysis_result_id values with truncated=false and nothing to say that
over half of index had no value at all. The analysis_result_id attribute
note points from the term filter to analysis_results.
Closes #43
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
🟢 Approval recommended
Static review identified no blocking issues in the shared filtering logic, adapter integration, or regression coverage; tests were not executed.
No open issues 🎉
What changed in this PR
Adds original-data-only and analysis-results-only cohort filtering across REST and MCP, addressing #43 without requiring SQL.
Changes:
- Adds
analysis_resultsmodes, validation, and matching download commands. - Exposes
null_countin attribute discovery. - Updates documentation and regression tests.
| File | Description |
|---|---|
| tests/test_filter_shape.py | Updates the default filter echo expectation. |
| tests/test_analysis_results_filter.py | Tests filtering, validation, parity, downloads, and NULL counts. |
| src/idc_api/rest/app.py | Updates request validation, examples, and descriptions. |
| src/idc_api/mcp/server.py | Exposes filter modes and updates agent guidance. |
| src/idc_api/core/services/manifest.py | Generates mode-aware download commands. |
| src/idc_api/core/services/discovery.py | Counts unset attribute values. |
| src/idc_api/core/schema.py | Explains how to select original data. |
| src/idc_api/core/models.py | Defines filter modes and null_count. |
| src/idc_api/core/filters.py | Compiles modes and enforces filter safeguards. |
| docs/user-guide.md | Documents filtering and download behavior. |
| CHANGELOG.md | Records user-visible additions. |
Review effort: Balanced
The download payload offered `idc download <collection>` for a filter of only collection_id values. A filter of only analysis_result_id values fell through to the manifest route alone, which also opened with "Or". `idc download <value>` resolves analysis_result_id too (idc-index 0.13.2+), so emit it for that case as well, and word the manifest line "Save" when it is the only option. The shape is now read from the applied filter rather than the request, so a predicate the compiler dropped can neither hide nor fake a whole-collection selection. Values are shlex-quoted: they come from the caller and land in a shell command the user may paste. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
idc download and idc download-from-manifest read each manifest line with pd.read_csv(header=None) as a single column, so one added field crashes them with a pandas "Length mismatch" error. Nothing recorded that: the only test checked that the first line starts with s3://, and #64 proposed widening the (JSON) manifest rows without anyone noting where the CLI's constraint actually lies. Make it architecture invariant #8 in CLAUDE.md and the developer guide, note it where the URLs are produced, and add tests that every line of manifest.txt and get_cohort_urls, for source=aws and gcs, is one bare s3://<bucket>/<crdc_series_uuid>/* URL, and that IDCClient's download_from_manifest (the path both CLI commands take, with only _s5cmd_run stubbed) resolves every line to an IDC series and to the right endpoint. Both tests fail when the lines gain a field. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #43. Cohort filters can now keep original data only, or analysis results only, so "NLST's original images" no longer needs SQL.
An analysis result's series carry the
collection_idof the images they analyze. In NLST that is 388,254 of 592,600 series (IDC v25), and the original data'sanalysis_result_idis NULL, so no term value could select it.analysis_resultsonCohortFilters"include"(default),"exclude"(original data only) or"only"(analysis results only), named and valued as in idc-index'sdownload_from_selection/idc download-from-selection --analysis-results.{"filters": {"terms": {"collection_id": ["nlst"]}, "analysis_results": "exclude"}}gives NLST's 204,346 original series, and the manifest endpoints take it too.
analysis_result_id IS NULL/IS NOT NULLon top oftermsandranges, and is echoed infilters_applied.cohort/counts,cohort/manifest,cohort/manifest.txt,citations,licenses, and the MCPbuild_cohort,get_cohort_urls,get_citationsandget_licenses. The MCP argument is aLiteral, so its tool schema carries the enum.require_filterstill refuses a manifest with noterms/ranges, because either side of the split is about half of IDC. The refusal says so.exclude+ ananalysis_result_idterm is a 400. That combination is always empty, and a zero would read as "no such data".idc download nlst, which fetches the analysis results too. Withexclude/onlyit now suggestsidc download-from-selection --collection-id nlst --analysis-results exclude.Download command for a whole analysis result
A filter of only
analysis_result_idvalues now getsidc download <analysis_result_id>(resolved by idc-index 0.13.2+ across every collection the analysis result spans), as a filter of onlycollection_idvalues already gotidc download <collection>. Before, it got the manifest route alone, worded "Or save…". The shape is read from the applied filter, and values areshlex-quoted because they land in a shell command.null_countinget_attribute_valuesSeries with no value are not in
values. Nownull_countreports them, soanalysis_result_idno longer reads as fully enumerated: 570,108 NULLs next totruncated: false. Theanalysis_result_idattribute note points from the term filter toanalysis_results.Why not
source_typeThe issue preferred a
source_typeterm (original_data/analysis_result). A mode field does the same job and matches the name idc-index users already know. It also needs no synthetic column in the attribute allow-list, sincesource_typeis not anindexcolumn. The issue's third acceptance item, theanalysis_result_iddescription, came in with idc-index-data 25.0.4 (#66).Text manifest pinned as an idc-index input format
This is a separate commit and doesn't depend on the rest.
idc downloadandidc download-from-manifestread each line ofmanifest.txt/get_cohort_urlsas a single CSV column, so one added field crashes them withValueError: Length mismatch. Until now nothing recorded this. #64's dropped commit widened the JSON rows, which are safe, but nothing would have stopped the same change landing on the text lines.CLAUDE.mdanddev/developer_guide.md, plus a docstring where the URLs are produced.tests/test_manifest_contract.py. It uses the two smallest series from each AWS bucket, so source=gcs remaps some of them and leaves the rest unchanged. Forawsandgcsit checks two things:manifest.txtline is exactlys3://<bucket>/<crdc_series_uuid>/*, andget_cohort_urlsreturns the same lines;IDCClient.download_from_manifest, the path both CLI commands take, resolves every line to an IDC series, the right endpoint and the expected total size. Only_s5cmd_runis stubbed, as idc-index's own tests do.,5to each line makes all four fail: two on the format check, two with idc-index's ownLength mismatch.Changes
core/models.py: theAnalysisResultsModeliteral,CohortFilters.analysis_resultsandAttributeValues.null_countcore/filters.py: compiling the new field, the contradiction check, and therequire_filterwordingcore/services/discovery.py: thenull_countquerycore/services/manifest.py: the split download command, the analysis-result command, quotingcore/schema.py: theanalysis_result_idnotemcp/server.py: the new tool arguments and docstrings, andidc://guiderest/app.py: docstrings, a counts example, and a top-levelanalysis_resultsis now named as a stray keydocs/user-guide.md,CHANGELOG.mdINSTRUCTIONSis unchanged: the guidance is in thebuild_cohortdescription andidc://guide.Test plan
tests/test_analysis_results_filter.py(new):IS NULLcountmanifest.txtline count equals the exclude countnull_countplus the listed values equals all seriestests/test_manifest_contract.py(see above)uv run pytest tests -q: 126 passed.ruff check,ruff format --checkandbanditare clean.🤖 Generated with Claude Code