pbcheck 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pbcheck-0.1.0/.gitattributes +24 -0
- pbcheck-0.1.0/.github/dependabot.yml +12 -0
- pbcheck-0.1.0/.github/workflows/census-candidates.yml +136 -0
- pbcheck-0.1.0/.github/workflows/release.yml +152 -0
- pbcheck-0.1.0/.github/workflows/tests.yml +171 -0
- pbcheck-0.1.0/.gitignore +57 -0
- pbcheck-0.1.0/.pre-commit-config.yaml +52 -0
- pbcheck-0.1.0/.zenodo.json +29 -0
- pbcheck-0.1.0/CHANGELOG.md +731 -0
- pbcheck-0.1.0/CITATION.cff +26 -0
- pbcheck-0.1.0/CONTRIBUTING.md +91 -0
- pbcheck-0.1.0/LICENSE +28 -0
- pbcheck-0.1.0/PKG-INFO +345 -0
- pbcheck-0.1.0/README.md +301 -0
- pbcheck-0.1.0/SECURITY.md +14 -0
- pbcheck-0.1.0/demo/README.md +121 -0
- pbcheck-0.1.0/demo/example_reference_two_arm/pbcheck_audit.json +738 -0
- pbcheck-0.1.0/demo/example_reference_two_arm/pbcheck_report.html +94 -0
- pbcheck-0.1.0/demo/example_reference_two_arm/pbcheck_report.md +413 -0
- pbcheck-0.1.0/demo/kang2018_design_gate/pbcheck_audit.json +228 -0
- pbcheck-0.1.0/demo/kang2018_design_gate/pbcheck_report.html +72 -0
- pbcheck-0.1.0/demo/kang2018_design_gate/pbcheck_report.md +196 -0
- pbcheck-0.1.0/docs/AMENDMENTS.md +3221 -0
- pbcheck-0.1.0/docs/ENV_NOTES.md +33 -0
- pbcheck-0.1.0/docs/PHASE0_SPEC.md +250 -0
- pbcheck-0.1.0/docs/PILOT_FINDINGS.md +205 -0
- pbcheck-0.1.0/docs/PREREGISTRATION_STRATUM_LIST.md +1084 -0
- pbcheck-0.1.0/docs/USAGE.md +240 -0
- pbcheck-0.1.0/pilot/README.md +184 -0
- pbcheck-0.1.0/pilot/gate/synthetic_gate_2026-08-15.json +266 -0
- pbcheck-0.1.0/pilot/preregistration/a2_feasibility_2026-08-16.csv +3521 -0
- pbcheck-0.1.0/pilot/preregistration/a2_feasibility_2026-08-16.json +98067 -0
- pbcheck-0.1.0/pilot/preregistration/census_candidates_run31910799023_2026-08-15.csv +2191 -0
- pbcheck-0.1.0/pilot/preregistration/census_candidates_run31910799023_2026-08-15.json +187984 -0
- pbcheck-0.1.0/pilot/preregistration/census_release_datasets_2025-01-30.json +17355 -0
- pbcheck-0.1.0/pilot/preregistration/discover_index_2026-08-16.json +58180 -0
- pbcheck-0.1.0/pilot/preregistration/stratum_list_2026-08-16.csv +358 -0
- pbcheck-0.1.0/pilot/preregistration/stratum_list_2026-08-16.json +30104 -0
- pbcheck-0.1.0/pilot/preregistration/stratum_list_proposal_2026-08-16.redacted.md +909 -0
- pbcheck-0.1.0/pilot/results/.gitkeep +0 -0
- pbcheck-0.1.0/pilot/testsel/summary.csv +147 -0
- pbcheck-0.1.0/pilot/testsel/summary.json +2776 -0
- pbcheck-0.1.0/pilot/upper_bound_check/upper_bound_check_2026-08-16.csv +11 -0
- pbcheck-0.1.0/pilot/upper_bound_check/upper_bound_check_2026-08-16.json +3471 -0
- pbcheck-0.1.0/pyproject.toml +107 -0
- pbcheck-0.1.0/requirements.lock +95 -0
- pbcheck-0.1.0/scripts/a2_feasibility.py +1103 -0
- pbcheck-0.1.0/scripts/analyze_test_selection.py +174 -0
- pbcheck-0.1.0/scripts/census_candidates.py +964 -0
- pbcheck-0.1.0/scripts/changelog_section.py +61 -0
- pbcheck-0.1.0/scripts/check_docstring_only_diff.py +150 -0
- pbcheck-0.1.0/scripts/check_upper_bound_claim.py +714 -0
- pbcheck-0.1.0/scripts/check_version_consistency.py +240 -0
- pbcheck-0.1.0/scripts/compare_gate_scalars.py +147 -0
- pbcheck-0.1.0/scripts/demo_common.py +240 -0
- pbcheck-0.1.0/scripts/demo_kang2018.py +117 -0
- pbcheck-0.1.0/scripts/demo_two_arm.py +257 -0
- pbcheck-0.1.0/scripts/fetch_preregistration_evidence.py +350 -0
- pbcheck-0.1.0/scripts/freeze_stratum_list.py +2442 -0
- pbcheck-0.1.0/scripts/measure_audit_runtime.py +300 -0
- pbcheck-0.1.0/scripts/pb_calibration_probe.py +1375 -0
- pbcheck-0.1.0/scripts/proof_of_life.py +140 -0
- pbcheck-0.1.0/scripts/protocol_safety_check.py +546 -0
- pbcheck-0.1.0/scripts/run_test_selection_grid.py +295 -0
- pbcheck-0.1.0/scripts/synthetic_gate.py +393 -0
- pbcheck-0.1.0/src/pbcheck/__init__.py +72 -0
- pbcheck-0.1.0/src/pbcheck/__main__.py +10 -0
- pbcheck-0.1.0/src/pbcheck/audit.py +1223 -0
- pbcheck-0.1.0/src/pbcheck/audit_schema.py +755 -0
- pbcheck-0.1.0/src/pbcheck/census_select.py +1084 -0
- pbcheck-0.1.0/src/pbcheck/cli.py +257 -0
- pbcheck-0.1.0/src/pbcheck/design.py +184 -0
- pbcheck-0.1.0/src/pbcheck/example.py +120 -0
- pbcheck-0.1.0/src/pbcheck/gate_config.py +210 -0
- pbcheck-0.1.0/src/pbcheck/gene_universe.py +74 -0
- pbcheck-0.1.0/src/pbcheck/io_counts.py +1483 -0
- pbcheck-0.1.0/src/pbcheck/methods/__init__.py +12 -0
- pbcheck-0.1.0/src/pbcheck/methods/de.py +50 -0
- pbcheck-0.1.0/src/pbcheck/methods/moderated.py +410 -0
- pbcheck-0.1.0/src/pbcheck/methods/naive.py +86 -0
- pbcheck-0.1.0/src/pbcheck/methods/naive_engine.py +434 -0
- pbcheck-0.1.0/src/pbcheck/methods/pseudobulk.py +254 -0
- pbcheck-0.1.0/src/pbcheck/metrics.py +217 -0
- pbcheck-0.1.0/src/pbcheck/mtc.py +151 -0
- pbcheck-0.1.0/src/pbcheck/permutation.py +358 -0
- pbcheck-0.1.0/src/pbcheck/product_constants.py +24 -0
- pbcheck-0.1.0/src/pbcheck/render/__init__.py +74 -0
- pbcheck-0.1.0/src/pbcheck/render/html.py +89 -0
- pbcheck-0.1.0/src/pbcheck/render/markdown.py +61 -0
- pbcheck-0.1.0/src/pbcheck/render/sections.py +682 -0
- pbcheck-0.1.0/src/pbcheck/render/text.py +550 -0
- pbcheck-0.1.0/synthetic/oracles.py +189 -0
- pbcheck-0.1.0/tests/conftest.py +102 -0
- pbcheck-0.1.0/tests/fixtures/audit_payload_coarse_null.html +96 -0
- pbcheck-0.1.0/tests/fixtures/audit_payload_coarse_null.json +389 -0
- pbcheck-0.1.0/tests/fixtures/audit_payload_coarse_null.md +341 -0
- pbcheck-0.1.0/tests/fixtures/audit_payload_complete.html +95 -0
- pbcheck-0.1.0/tests/fixtures/audit_payload_complete.json +415 -0
- pbcheck-0.1.0/tests/fixtures/audit_payload_complete.md +365 -0
- pbcheck-0.1.0/tests/fixtures/audit_payload_design_only.html +74 -0
- pbcheck-0.1.0/tests/fixtures/audit_payload_design_only.json +234 -0
- pbcheck-0.1.0/tests/fixtures/audit_payload_design_only.md +198 -0
- pbcheck-0.1.0/tests/fixtures/audit_payload_naive_only.html +87 -0
- pbcheck-0.1.0/tests/fixtures/audit_payload_naive_only.json +348 -0
- pbcheck-0.1.0/tests/fixtures/audit_payload_naive_only.md +288 -0
- pbcheck-0.1.0/tests/test_a2_feasibility.py +317 -0
- pbcheck-0.1.0/tests/test_audit.py +1069 -0
- pbcheck-0.1.0/tests/test_audit_schema.py +448 -0
- pbcheck-0.1.0/tests/test_census_candidates.py +792 -0
- pbcheck-0.1.0/tests/test_census_select.py +800 -0
- pbcheck-0.1.0/tests/test_checklist_scripts.py +400 -0
- pbcheck-0.1.0/tests/test_cli.py +357 -0
- pbcheck-0.1.0/tests/test_demo_scripts.py +359 -0
- pbcheck-0.1.0/tests/test_design.py +115 -0
- pbcheck-0.1.0/tests/test_docs.py +608 -0
- pbcheck-0.1.0/tests/test_engine_oracles.py +68 -0
- pbcheck-0.1.0/tests/test_example.py +81 -0
- pbcheck-0.1.0/tests/test_gate_config.py +115 -0
- pbcheck-0.1.0/tests/test_gene_universe.py +65 -0
- pbcheck-0.1.0/tests/test_grid_summary.py +130 -0
- pbcheck-0.1.0/tests/test_io_counts.py +783 -0
- pbcheck-0.1.0/tests/test_measure_audit_runtime.py +34 -0
- pbcheck-0.1.0/tests/test_methods.py +202 -0
- pbcheck-0.1.0/tests/test_metrics.py +144 -0
- pbcheck-0.1.0/tests/test_moderated.py +146 -0
- pbcheck-0.1.0/tests/test_naive_engine.py +702 -0
- pbcheck-0.1.0/tests/test_oracles.py +141 -0
- pbcheck-0.1.0/tests/test_packaging.py +443 -0
- pbcheck-0.1.0/tests/test_permutation_mtc.py +220 -0
- pbcheck-0.1.0/tests/test_properties.py +103 -0
- pbcheck-0.1.0/tests/test_protocol_safety_check.py +533 -0
- pbcheck-0.1.0/tests/test_pseudobulk.py +117 -0
- pbcheck-0.1.0/tests/test_render.py +241 -0
- pbcheck-0.1.0/tests/test_render_output.py +135 -0
- pbcheck-0.1.0/tests/test_render_text.py +363 -0
- pbcheck-0.1.0/tests/test_stratum_list_freeze.py +1646 -0
- pbcheck-0.1.0/tests/test_synthetic_gate.py +128 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# The pre-registration artifacts are hash-pinned, not read as text.
|
|
2
|
+
#
|
|
3
|
+
# `pilot/preregistration/` holds the frozen §1 stratum list and the CI candidate manifest it was
|
|
4
|
+
# derived from. `docs/PREREGISTRATION_STRATUM_LIST.md` records their sha256 sums, and
|
|
5
|
+
# `scripts/freeze_stratum_list.py` refuses to run against a source whose hash does not match —
|
|
6
|
+
# so their bytes are the evidence, not a rendering of it.
|
|
7
|
+
#
|
|
8
|
+
# Without `-text`, git's end-of-line conversion would rewrite them per platform: the committed CSV
|
|
9
|
+
# came off a Linux runner with CRLF terminators, `core.autocrlf=true` on a Windows checkout would
|
|
10
|
+
# rewrite the JSON to CRLF, and either change breaks the recorded hash. The byte-identity test in
|
|
11
|
+
# `tests/test_stratum_list_freeze.py` would then pass on one CI leg and fail on another, which is
|
|
12
|
+
# worse than failing everywhere. Diffing these files is not how they are audited; hashing them is.
|
|
13
|
+
#
|
|
14
|
+
# `pilot/gate/` and `pilot/testsel/` are cited evidence too — `.pre-commit-config.yaml` excludes
|
|
15
|
+
# them from the fixing hooks on exactly that ground — and they are deliberately NOT marked here.
|
|
16
|
+
# They were committed with LF and are checked out with CRLF under `core.autocrlf=true`, so adding
|
|
17
|
+
# `-text` would make the working tree's CRLF bytes canonical and the next `git add` would rewrite
|
|
18
|
+
# all three blobs: measured, at the commit that added this line, 8909 -> 9174 bytes for
|
|
19
|
+
# `pilot/gate/synthetic_gate_2026-08-15.json`, 21811 -> 21958 for `pilot/testsel/summary.csv` and
|
|
20
|
+
# 63081 -> 65856 for `pilot/testsel/summary.json`. Protecting evidence by rewriting it is not
|
|
21
|
+
# protecting it. The asymmetry is therefore real and intended: `pilot/preregistration/` is pinned by
|
|
22
|
+
# hash and must be byte-stable across platforms; the other two are protected from the fixing hooks
|
|
23
|
+
# only, and their bytes stay whatever git already stores.
|
|
24
|
+
pilot/preregistration/** -text
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
version: 2
|
|
2
|
+
updates:
|
|
3
|
+
# github-actions only, monthly. pip is deliberately NOT enrolled here: this is a bleeding-edge
|
|
4
|
+
# scientific stack (numpy 2.4 / pandas 3.0 / scanpy 1.12 — see docs/ENV_NOTES.md) where a weekly
|
|
5
|
+
# pip-update PR would mostly be noise a solo maintainer has to triage, not a real signal. The
|
|
6
|
+
# CI "Report the resolved scientific stack" step (.github/workflows/tests.yml, `test` job) is
|
|
7
|
+
# the drift detector by design instead: it prints what actually resolved on every push, so
|
|
8
|
+
# drift is visible without a bot filing a PR nobody asked for.
|
|
9
|
+
- package-ecosystem: "github-actions"
|
|
10
|
+
directory: "/"
|
|
11
|
+
schedule:
|
|
12
|
+
interval: "monthly"
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# The one job in this repo that touches real data. It reads the whole pinned CELLxGENE Census
|
|
2
|
+
# (~60 M cells after the §1 value_filter) and produces the CANDIDATE stratum manifest — not the
|
|
3
|
+
# §1 pre-registration, and not an admission of anything: every row carries admitted_to_sweep=False.
|
|
4
|
+
#
|
|
5
|
+
# Manual only. It is multi-hour, it reaches the network for gigabytes, and its result is an
|
|
6
|
+
# artifact rather than a commit, so there is nothing for a push or a pull request to gain from it.
|
|
7
|
+
# The regular `tests` workflow deliberately does NOT install the [census] extra; this one does, and
|
|
8
|
+
# it is the only place a cellxgene-census / tiledbsoma resolution is exercised at all.
|
|
9
|
+
name: census candidates
|
|
10
|
+
|
|
11
|
+
on:
|
|
12
|
+
workflow_dispatch:
|
|
13
|
+
inputs:
|
|
14
|
+
dry_run:
|
|
15
|
+
description: "Dry run: pass 2 reads only a handful of datasets (spread across the size range) to calibrate the full run. Uncheck for the real thing."
|
|
16
|
+
type: boolean
|
|
17
|
+
default: true
|
|
18
|
+
max_datasets:
|
|
19
|
+
description: "Cap the datasets pass 2 reads, largest first (0 = no cap)."
|
|
20
|
+
type: number
|
|
21
|
+
default: 0
|
|
22
|
+
artifact_retention_days:
|
|
23
|
+
description: "How long to keep the manifest + log artifact."
|
|
24
|
+
type: number
|
|
25
|
+
default: 30
|
|
26
|
+
reader_buffer_mb:
|
|
27
|
+
description: "Per-column Census read buffer, MiB (py./soma.init_buffer_bytes). The census default is 1024, which four string columns do not fit into a 16 GB runner. Lower this if the run is killed at exit 143."
|
|
28
|
+
type: number
|
|
29
|
+
default: 128
|
|
30
|
+
|
|
31
|
+
# Deliberately NOT cancel-in-progress (the `tests` workflow is): a five-hour run that is killed
|
|
32
|
+
# by a second dispatch produces nothing at all, and the Census is pinned, so two concurrent runs
|
|
33
|
+
# would compute the same answer twice. Queue instead.
|
|
34
|
+
concurrency:
|
|
35
|
+
group: ${{ github.workflow }}
|
|
36
|
+
cancel-in-progress: false
|
|
37
|
+
|
|
38
|
+
# The Census is public and anonymous — no secrets, no credentials, nothing to write.
|
|
39
|
+
permissions:
|
|
40
|
+
contents: read
|
|
41
|
+
|
|
42
|
+
jobs:
|
|
43
|
+
candidates:
|
|
44
|
+
name: candidate manifest (dry_run=${{ inputs.dry_run }})
|
|
45
|
+
runs-on: ubuntu-latest
|
|
46
|
+
# A public runner is killed at 6 h with no artifact. Stopping at 5.5 h leaves the upload step
|
|
47
|
+
# (if: always()) time to run, so a run that hits the wall still returns its partial log — which
|
|
48
|
+
# is what makes the next dispatch's budget an informed decision rather than a guess.
|
|
49
|
+
timeout-minutes: 330
|
|
50
|
+
|
|
51
|
+
steps:
|
|
52
|
+
- uses: actions/checkout@v7
|
|
53
|
+
|
|
54
|
+
- uses: actions/setup-python@v7
|
|
55
|
+
with:
|
|
56
|
+
python-version: "3.12"
|
|
57
|
+
cache: pip
|
|
58
|
+
cache-dependency-path: pyproject.toml
|
|
59
|
+
|
|
60
|
+
# cellxgene-census is unpinned in pyproject on purpose: the SDK is a client for a pinned
|
|
61
|
+
# dataset, and package_versions() records whatever actually resolved into the manifest
|
|
62
|
+
# header. The pin that matters — the Census version — is in census_select.py, not here.
|
|
63
|
+
- name: Install the package with the [census] extra
|
|
64
|
+
run: |
|
|
65
|
+
python -m pip install --upgrade pip
|
|
66
|
+
python -m pip install -e ".[census]"
|
|
67
|
+
|
|
68
|
+
# The counterpart of the `tests` workflow's stack report, for the half of the stack that
|
|
69
|
+
# workflow never installs. The Census version and the value_filter are printed from the
|
|
70
|
+
# package so the log states the protocol the run was made under, not a copy of it. It is
|
|
71
|
+
# tee'd into the same log the artifact carries, so a run that dies in the driver still ships
|
|
72
|
+
# the stack it died with — the log is the post-mortem, and it starts here.
|
|
73
|
+
- name: Report the resolved Census stack and the pinned protocol
|
|
74
|
+
shell: bash
|
|
75
|
+
run: |
|
|
76
|
+
{
|
|
77
|
+
python -c "import cellxgene_census, tiledbsoma, pyarrow, pandas, numpy; [print(f'{m.__name__}=={m.__version__}') for m in (cellxgene_census, tiledbsoma, pyarrow, pandas, numpy)]"
|
|
78
|
+
python -c "from pbcheck import census_select as cs; print('census_version:', cs.CENSUS_VERSION); print('value_filter (applied):', cs.VALUE_FILTER); print('value_filter (spec text):', cs.SPEC_VALUE_FILTER); print('organism:', cs.CENSUS_ORGANISM)"
|
|
79
|
+
free -m || true
|
|
80
|
+
nproc || true
|
|
81
|
+
} 2>&1 | tee census-candidates.log
|
|
82
|
+
|
|
83
|
+
# GitHub runs `run:` as `bash -e -o pipefail`, so the driver failing anywhere in the pipe
|
|
84
|
+
# fails this step — which is what we want, and is why the job summary is NOT written here.
|
|
85
|
+
# The previous version put the summary after the pipeline in this same step: `-e` (which
|
|
86
|
+
# `set -uo pipefail` does not clear) aborted the step at the pipe, and neither the summary
|
|
87
|
+
# nor the intended exit code ever ran. Reporting belongs in a step that cannot be skipped.
|
|
88
|
+
- name: Build the candidate manifest
|
|
89
|
+
shell: bash
|
|
90
|
+
env:
|
|
91
|
+
DRY_RUN: ${{ inputs.dry_run }}
|
|
92
|
+
MAX_DATASETS: ${{ inputs.max_datasets }}
|
|
93
|
+
READER_BUFFER_MB: ${{ inputs.reader_buffer_mb }}
|
|
94
|
+
# Cell-type labels are ontology free text and carry non-ASCII ("Muller cell" is spelled
|
|
95
|
+
# with an umlaut in CL), and the driver prints them in its top-candidates summary. If the
|
|
96
|
+
# runner's stdout ever resolved to ASCII, that print would die AFTER the manifest was
|
|
97
|
+
# written — a red run, and a lost log, for a formatting reason.
|
|
98
|
+
PYTHONIOENCODING: utf-8
|
|
99
|
+
run: |
|
|
100
|
+
args=(--max-datasets "${MAX_DATASETS:-0}" --reader-buffer-mb "${READER_BUFFER_MB:-128}")
|
|
101
|
+
if [ "${DRY_RUN}" = "true" ]; then
|
|
102
|
+
args+=(--dry-run)
|
|
103
|
+
fi
|
|
104
|
+
echo "python scripts/census_candidates.py ${args[*]}" | tee -a census-candidates.log
|
|
105
|
+
python -u scripts/census_candidates.py "${args[@]}" 2>&1 | tee -a census-candidates.log
|
|
106
|
+
|
|
107
|
+
# if: always() — this is the step that reports, so it must run when the driver dies, when it
|
|
108
|
+
# times out, and when it succeeds. A memory kill (SIGTERM, exit 143) produces no traceback:
|
|
109
|
+
# the tail of this log is the entire post-mortem.
|
|
110
|
+
- name: Summarise the run log
|
|
111
|
+
if: always()
|
|
112
|
+
shell: bash
|
|
113
|
+
run: |
|
|
114
|
+
{
|
|
115
|
+
echo "### Census candidate manifest (dry_run=${{ inputs.dry_run }}, buffers ${{ inputs.reader_buffer_mb }} MiB)"
|
|
116
|
+
echo
|
|
117
|
+
echo '```'
|
|
118
|
+
tail -n 80 census-candidates.log 2>/dev/null || echo "(no log — the run died before writing one)"
|
|
119
|
+
echo '```'
|
|
120
|
+
} >> "$GITHUB_STEP_SUMMARY"
|
|
121
|
+
|
|
122
|
+
# if: always() — a partial log from a run that timed out or died mid-pass is the timing
|
|
123
|
+
# calibration for the next dispatch, and losing it costs another five hours to recover.
|
|
124
|
+
# The manifest itself is uploaded, never committed: pilot/results/ is gitignored, and a
|
|
125
|
+
# candidate list that lands in git by way of a CI job has pre-registered itself by accident.
|
|
126
|
+
- name: Upload the manifest and the run log
|
|
127
|
+
uses: actions/upload-artifact@v6
|
|
128
|
+
if: always()
|
|
129
|
+
with:
|
|
130
|
+
name: census-candidates-${{ github.run_id }}
|
|
131
|
+
path: |
|
|
132
|
+
pilot/results/census_candidates/*.json
|
|
133
|
+
pilot/results/census_candidates/*.csv
|
|
134
|
+
census-candidates.log
|
|
135
|
+
retention-days: ${{ inputs.artifact_retention_days }}
|
|
136
|
+
if-no-files-found: warn
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
name: release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*"]
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
inputs:
|
|
8
|
+
target:
|
|
9
|
+
type: choice
|
|
10
|
+
options: [testpypi]
|
|
11
|
+
default: testpypi
|
|
12
|
+
|
|
13
|
+
permissions:
|
|
14
|
+
contents: read
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
check:
|
|
18
|
+
name: lint + version check + tests
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v7
|
|
22
|
+
with:
|
|
23
|
+
# A tag push checks out the tag alone; tests/test_protocol_safety_check.py needs
|
|
24
|
+
# origin/main to diff against, so fetch the full history here as tests.yml does.
|
|
25
|
+
fetch-depth: 0
|
|
26
|
+
|
|
27
|
+
- uses: actions/setup-python@v7
|
|
28
|
+
with:
|
|
29
|
+
python-version: "3.12"
|
|
30
|
+
cache: pip
|
|
31
|
+
cache-dependency-path: pyproject.toml
|
|
32
|
+
|
|
33
|
+
- name: Install package (editable) + dev extra
|
|
34
|
+
run: |
|
|
35
|
+
python -m pip install --upgrade pip
|
|
36
|
+
python -m pip install -e ".[dev]"
|
|
37
|
+
|
|
38
|
+
- name: ruff check
|
|
39
|
+
run: python -m ruff check .
|
|
40
|
+
|
|
41
|
+
- name: Check CITATION.cff / CHANGELOG.md version match the package
|
|
42
|
+
run: python scripts/check_version_consistency.py
|
|
43
|
+
|
|
44
|
+
- name: Reject malformed release tags
|
|
45
|
+
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
|
|
46
|
+
run: |
|
|
47
|
+
if ! [[ "$GITHUB_REF_NAME" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
|
48
|
+
echo "tag '$GITHUB_REF_NAME' does not match v[0-9]+.[0-9]+.[0-9]+" >&2
|
|
49
|
+
exit 1
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
- name: Check the tag matches the released version
|
|
53
|
+
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
|
|
54
|
+
run: python scripts/check_version_consistency.py --tag "$GITHUB_REF_NAME" --require-date
|
|
55
|
+
|
|
56
|
+
- name: Run tests
|
|
57
|
+
run: python -m pytest -q -m "not slow"
|
|
58
|
+
|
|
59
|
+
build:
|
|
60
|
+
name: build sdist/wheel + twine check
|
|
61
|
+
needs: check
|
|
62
|
+
runs-on: ubuntu-latest
|
|
63
|
+
steps:
|
|
64
|
+
- uses: actions/checkout@v7
|
|
65
|
+
|
|
66
|
+
- uses: actions/setup-python@v7
|
|
67
|
+
with:
|
|
68
|
+
python-version: "3.12"
|
|
69
|
+
cache: pip
|
|
70
|
+
cache-dependency-path: pyproject.toml
|
|
71
|
+
|
|
72
|
+
- name: Install packaging tools
|
|
73
|
+
run: |
|
|
74
|
+
python -m pip install --upgrade pip
|
|
75
|
+
python -m pip install build twine
|
|
76
|
+
|
|
77
|
+
- name: Build sdist + wheel
|
|
78
|
+
run: python -m build
|
|
79
|
+
|
|
80
|
+
- name: twine check
|
|
81
|
+
run: python -m twine check dist/*
|
|
82
|
+
|
|
83
|
+
- uses: actions/upload-artifact@v6
|
|
84
|
+
with:
|
|
85
|
+
name: dist
|
|
86
|
+
path: dist/
|
|
87
|
+
|
|
88
|
+
publish-testpypi:
|
|
89
|
+
name: publish to TestPyPI
|
|
90
|
+
needs: build
|
|
91
|
+
if: github.event_name == 'workflow_dispatch' && inputs.target == 'testpypi'
|
|
92
|
+
runs-on: ubuntu-latest
|
|
93
|
+
environment: testpypi
|
|
94
|
+
permissions:
|
|
95
|
+
id-token: write
|
|
96
|
+
steps:
|
|
97
|
+
- uses: actions/download-artifact@v6
|
|
98
|
+
with:
|
|
99
|
+
name: dist
|
|
100
|
+
path: dist/
|
|
101
|
+
|
|
102
|
+
# Pinned by commit SHA, not the release/v1 branch: a job holding id-token: write should
|
|
103
|
+
# not follow a moving ref. dc37677b2e1c63e2034f94d8a5b11f265b73ba33 == tag v1.14.2.
|
|
104
|
+
- uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
|
|
105
|
+
with:
|
|
106
|
+
repository-url: https://test.pypi.org/legacy/
|
|
107
|
+
|
|
108
|
+
publish-pypi:
|
|
109
|
+
name: publish to PyPI
|
|
110
|
+
needs: build
|
|
111
|
+
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
|
|
112
|
+
runs-on: ubuntu-latest
|
|
113
|
+
environment: pypi
|
|
114
|
+
permissions:
|
|
115
|
+
id-token: write
|
|
116
|
+
steps:
|
|
117
|
+
- uses: actions/download-artifact@v6
|
|
118
|
+
with:
|
|
119
|
+
name: dist
|
|
120
|
+
path: dist/
|
|
121
|
+
|
|
122
|
+
# Pinned by commit SHA, not the release/v1 branch: a job holding id-token: write should
|
|
123
|
+
# not follow a moving ref. dc37677b2e1c63e2034f94d8a5b11f265b73ba33 == tag v1.14.2.
|
|
124
|
+
- uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
|
|
125
|
+
|
|
126
|
+
github-release:
|
|
127
|
+
name: create the GitHub Release
|
|
128
|
+
needs: publish-pypi
|
|
129
|
+
runs-on: ubuntu-latest
|
|
130
|
+
permissions:
|
|
131
|
+
contents: write
|
|
132
|
+
steps:
|
|
133
|
+
- uses: actions/checkout@v7
|
|
134
|
+
|
|
135
|
+
- uses: actions/setup-python@v7
|
|
136
|
+
with:
|
|
137
|
+
python-version: "3.12"
|
|
138
|
+
cache: pip
|
|
139
|
+
cache-dependency-path: pyproject.toml
|
|
140
|
+
|
|
141
|
+
- uses: actions/download-artifact@v6
|
|
142
|
+
with:
|
|
143
|
+
name: dist
|
|
144
|
+
path: dist/
|
|
145
|
+
|
|
146
|
+
- name: Extract the CHANGELOG section for this release
|
|
147
|
+
run: python scripts/changelog_section.py "${GITHUB_REF_NAME#v}" > notes.md
|
|
148
|
+
|
|
149
|
+
- name: Create the GitHub Release
|
|
150
|
+
env:
|
|
151
|
+
GH_TOKEN: ${{ github.token }}
|
|
152
|
+
run: gh release create "$GITHUB_REF_NAME" dist/* --title "pbcheck $GITHUB_REF_NAME" --notes-file notes.md
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
name: tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
concurrency:
|
|
10
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
11
|
+
cancel-in-progress: true
|
|
12
|
+
|
|
13
|
+
permissions:
|
|
14
|
+
contents: read
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
lint:
|
|
18
|
+
name: ruff check
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v7
|
|
22
|
+
|
|
23
|
+
- uses: actions/setup-python@v7
|
|
24
|
+
with:
|
|
25
|
+
python-version: "3.12"
|
|
26
|
+
cache: pip
|
|
27
|
+
cache-dependency-path: pyproject.toml
|
|
28
|
+
|
|
29
|
+
- name: Install dev extra (ruff only, no scientific stack needed for lint)
|
|
30
|
+
run: |
|
|
31
|
+
python -m pip install --upgrade pip
|
|
32
|
+
python -m pip install -e ".[dev]"
|
|
33
|
+
|
|
34
|
+
# ruff format --check is deliberately NOT run here: this is a pre-registered scientific
|
|
35
|
+
# repo, and a lot of existing code predates a formatting policy. Reformatting it wholesale
|
|
36
|
+
# in a lint-hardening commit would pollute `git blame` on files the analysis depends on.
|
|
37
|
+
# `ruff check` (real lint rules: unused imports, etc.) is enforced; formatting is not yet.
|
|
38
|
+
- name: ruff check
|
|
39
|
+
run: python -m ruff check .
|
|
40
|
+
|
|
41
|
+
# pyproject.toml's version is dynamic, read from src/pbcheck/__init__.py by hatchling
|
|
42
|
+
# (R3); CITATION.cff keeps an independent copy for tools that read it standalone (GitHub's
|
|
43
|
+
# "Cite this repository", Zenodo), which is exactly the copy that can drift unnoticed.
|
|
44
|
+
- name: Check CITATION.cff version matches the package
|
|
45
|
+
run: python scripts/check_version_consistency.py
|
|
46
|
+
|
|
47
|
+
test:
|
|
48
|
+
name: pytest (py${{ matrix.python-version }}, ${{ matrix.os }})
|
|
49
|
+
runs-on: ${{ matrix.os }}
|
|
50
|
+
strategy:
|
|
51
|
+
fail-fast: false
|
|
52
|
+
matrix:
|
|
53
|
+
os: [ubuntu-latest]
|
|
54
|
+
python-version: ["3.12", "3.13"]
|
|
55
|
+
include:
|
|
56
|
+
# Developed on Windows; Linux is the untested platform, so it carries the matrix.
|
|
57
|
+
- os: windows-latest
|
|
58
|
+
python-version: "3.12"
|
|
59
|
+
# One macOS combination, on the newer interpreter, for a third-platform smoke check
|
|
60
|
+
# without tripling the whole matrix.
|
|
61
|
+
- os: macos-latest
|
|
62
|
+
python-version: "3.13"
|
|
63
|
+
|
|
64
|
+
steps:
|
|
65
|
+
- uses: actions/checkout@v7
|
|
66
|
+
with:
|
|
67
|
+
# tests/test_protocol_safety_check.py runs the checklist against main; a branch or
|
|
68
|
+
# pull-request checkout at the default depth 1 carries no main to diff against.
|
|
69
|
+
fetch-depth: 0
|
|
70
|
+
|
|
71
|
+
- uses: actions/setup-python@v7
|
|
72
|
+
with:
|
|
73
|
+
python-version: ${{ matrix.python-version }}
|
|
74
|
+
cache: pip
|
|
75
|
+
cache-dependency-path: pyproject.toml
|
|
76
|
+
|
|
77
|
+
# The [census] extra is deliberately NOT installed: Phase-0 engine validation runs
|
|
78
|
+
# entirely on synthetic oracles, so CI never reaches the network for data.
|
|
79
|
+
- name: Install package (src-layout, editable) + dev extra
|
|
80
|
+
run: |
|
|
81
|
+
python -m pip install --upgrade pip
|
|
82
|
+
python -m pip install -e ".[dev]"
|
|
83
|
+
|
|
84
|
+
# Not decoration: this turns the CI log into a live counterpart of docs/ENV_NOTES.md
|
|
85
|
+
# and shows immediately if an unpinned upper bound drifts.
|
|
86
|
+
- name: Report the resolved scientific stack
|
|
87
|
+
run: |
|
|
88
|
+
python -c "import numpy, scipy, pandas, anndata, scanpy, decoupler, pydeseq2, statsmodels; [print(f'{m.__name__}=={m.__version__}') for m in (numpy, scipy, pandas, anndata, scanpy, decoupler, pydeseq2, statsmodels)]"
|
|
89
|
+
|
|
90
|
+
# Report-only: no --cov-fail-under gate. This is a bleeding-edge, pre-registered research
|
|
91
|
+
# repo mid-Phase-0 — a coverage floor would either block legitimate work or get raised by
|
|
92
|
+
# weakening it, neither of which is worth it yet. The number is surfaced so it is visible
|
|
93
|
+
# without being enforced.
|
|
94
|
+
- name: Run tests (with coverage)
|
|
95
|
+
shell: bash
|
|
96
|
+
run: |
|
|
97
|
+
set -euo pipefail
|
|
98
|
+
python -m pytest -q --cov=pbcheck --cov-report=term | tee pytest-coverage.txt
|
|
99
|
+
{
|
|
100
|
+
echo "### Coverage (pbcheck) — report-only, no threshold gate"
|
|
101
|
+
echo
|
|
102
|
+
echo '```'
|
|
103
|
+
cat pytest-coverage.txt
|
|
104
|
+
echo '```'
|
|
105
|
+
} >> "$GITHUB_STEP_SUMMARY"
|
|
106
|
+
|
|
107
|
+
lock:
|
|
108
|
+
name: pytest from requirements.lock (reproducibility)
|
|
109
|
+
runs-on: ubuntu-latest
|
|
110
|
+
steps:
|
|
111
|
+
- uses: actions/checkout@v7
|
|
112
|
+
with:
|
|
113
|
+
# tests/test_protocol_safety_check.py runs the checklist against main; a branch or
|
|
114
|
+
# pull-request checkout at the default depth 1 carries no main to diff against.
|
|
115
|
+
fetch-depth: 0
|
|
116
|
+
|
|
117
|
+
- uses: actions/setup-python@v7
|
|
118
|
+
with:
|
|
119
|
+
python-version: "3.12"
|
|
120
|
+
cache: pip
|
|
121
|
+
cache-dependency-path: requirements.lock
|
|
122
|
+
|
|
123
|
+
# Install FROM the frozen lock, then the package itself with --no-deps so pip cannot
|
|
124
|
+
# reach past the lock for anything. This is the reproducibility job: it answers "does the
|
|
125
|
+
# exact stack that generated requirements.lock still work?" The `test` job above answers a
|
|
126
|
+
# different question — "does a fresh resolve of pyproject.toml's unpinned bounds still
|
|
127
|
+
# work?" — and is the drift detector; this job does not replace it.
|
|
128
|
+
- name: Install from requirements.lock
|
|
129
|
+
run: |
|
|
130
|
+
python -m pip install --upgrade pip
|
|
131
|
+
python -m pip install -r requirements.lock
|
|
132
|
+
python -m pip install -e . --no-deps
|
|
133
|
+
|
|
134
|
+
- name: Run tests
|
|
135
|
+
run: python -m pytest -q
|
|
136
|
+
|
|
137
|
+
build:
|
|
138
|
+
name: build sdist/wheel + twine check + install smoke test
|
|
139
|
+
runs-on: ubuntu-latest
|
|
140
|
+
steps:
|
|
141
|
+
- uses: actions/checkout@v7
|
|
142
|
+
|
|
143
|
+
- uses: actions/setup-python@v7
|
|
144
|
+
with:
|
|
145
|
+
python-version: "3.12"
|
|
146
|
+
cache: pip
|
|
147
|
+
cache-dependency-path: pyproject.toml
|
|
148
|
+
|
|
149
|
+
# build/twine are packaging tooling, not part of the [dev] test/lint extra, so they are
|
|
150
|
+
# installed directly here rather than pulled in by every contributor's dev install.
|
|
151
|
+
- name: Install packaging tools
|
|
152
|
+
run: |
|
|
153
|
+
python -m pip install --upgrade pip
|
|
154
|
+
python -m pip install build twine
|
|
155
|
+
|
|
156
|
+
- name: Build sdist + wheel
|
|
157
|
+
run: python -m build
|
|
158
|
+
|
|
159
|
+
- name: twine check
|
|
160
|
+
run: python -m twine check dist/*
|
|
161
|
+
|
|
162
|
+
# The real smoke test: install the built wheel (not the editable checkout) into a clean
|
|
163
|
+
# venv and confirm the package actually imports — catches missing package-data / MANIFEST
|
|
164
|
+
# issues that `pip install -e .` can silently paper over.
|
|
165
|
+
- name: Install the built wheel into a fresh venv and import it
|
|
166
|
+
run: |
|
|
167
|
+
python -m venv wheel-venv
|
|
168
|
+
wheel-venv/bin/pip install --upgrade pip
|
|
169
|
+
wheel-venv/bin/pip install dist/*.whl
|
|
170
|
+
wheel-venv/bin/python -c "import pbcheck; print(pbcheck.__version__)"
|
|
171
|
+
wheel-venv/bin/pbcheck --version
|
pbcheck-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.eggs/
|
|
6
|
+
build/
|
|
7
|
+
dist/
|
|
8
|
+
.pytest_cache/
|
|
9
|
+
.hypothesis/
|
|
10
|
+
.mypy_cache/
|
|
11
|
+
.ruff_cache/
|
|
12
|
+
|
|
13
|
+
# Environments
|
|
14
|
+
.venv/
|
|
15
|
+
venv/
|
|
16
|
+
env/
|
|
17
|
+
.env
|
|
18
|
+
|
|
19
|
+
# Data & caches (never commit downloaded datasets or the census cache)
|
|
20
|
+
data/
|
|
21
|
+
*.h5ad
|
|
22
|
+
*.h5
|
|
23
|
+
*.loom
|
|
24
|
+
cellxgene_census_cache/
|
|
25
|
+
*.zarr/
|
|
26
|
+
*.tiledb/
|
|
27
|
+
*.mtx
|
|
28
|
+
*.tsv.gz
|
|
29
|
+
|
|
30
|
+
# Run outputs: ignore everything, keep the directory.
|
|
31
|
+
pilot/results/*
|
|
32
|
+
!pilot/results/.gitkeep
|
|
33
|
+
pilot/cache/
|
|
34
|
+
|
|
35
|
+
# Coverage & logs
|
|
36
|
+
.coverage
|
|
37
|
+
htmlcov/
|
|
38
|
+
*.log
|
|
39
|
+
|
|
40
|
+
# Editors
|
|
41
|
+
.vscode/
|
|
42
|
+
.idea/
|
|
43
|
+
|
|
44
|
+
# Notebooks
|
|
45
|
+
.ipynb_checkpoints/
|
|
46
|
+
|
|
47
|
+
# OS
|
|
48
|
+
.DS_Store
|
|
49
|
+
Thumbs.db
|
|
50
|
+
|
|
51
|
+
# Amendment 4 Part A's upper-bound reproducer keeps a resume ledger beside its artifact. The
|
|
52
|
+
# committed record is the dated JSON/CSV, which already carries every per-seed replicate; the
|
|
53
|
+
# ledger is a cache, following pilot/testsel/ committing only its summary.
|
|
54
|
+
pilot/upper_bound_check/replicates.jsonl
|
|
55
|
+
|
|
56
|
+
# Claude Code session scratch (worktrees are ephemeral; agents and skills ARE tracked)
|
|
57
|
+
.claude/worktrees/
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Mirrors CI (.github/workflows/tests.yml) on purpose: `ruff check` only, no `--fix` and no
|
|
2
|
+
# format hooks. This is a pre-registered scientific repo and a lot of existing code predates a
|
|
3
|
+
# formatting policy — see the comment on the lint job in tests.yml for why `ruff format --check`
|
|
4
|
+
# is deliberately absent there too. A pre-commit hook that reformatted on commit would diverge
|
|
5
|
+
# from what CI enforces and would pollute `git blame` on files the analysis depends on.
|
|
6
|
+
#
|
|
7
|
+
# docs/PHASE0_SPEC.md and docs/AMENDMENTS.md are the frozen protocol documents (see CLAUDE.md /
|
|
8
|
+
# the R3 task brief) and are excluded from the fixing hooks below so their bytes are never
|
|
9
|
+
# touched by a mechanical pass — end-of-file-fixer would otherwise add PHASE0_SPEC.md's missing
|
|
10
|
+
# trailing newline.
|
|
11
|
+
#
|
|
12
|
+
# The committed RUN ARTIFACTS under pilot/ are excluded for the same reason, one degree stronger:
|
|
13
|
+
# they are cited evidence, and pilot/preregistration/ is additionally pinned BY HASH in
|
|
14
|
+
# docs/PREREGISTRATION_STRATUM_LIST.md and by scripts/freeze_stratum_list.py, which refuses to run
|
|
15
|
+
# against a source whose sha256 has moved. Two of them end without a trailing newline, because
|
|
16
|
+
# json.dump writes none: the CI candidate manifest, and pilot/gate/synthetic_gate_2026-08-15.json.
|
|
17
|
+
# This was not hypothetical — running `pre-commit run --all-files` while adding the freeze rewrote
|
|
18
|
+
# the gate artifact, whose numbers Amendment 3's addendum, the README and CHANGELOG all quote.
|
|
19
|
+
# The hooks had simply never been run over it (pre-commit is configured here but not installed as
|
|
20
|
+
# a git hook). A mechanical pass must not be able to edit evidence, so it no longer can.
|
|
21
|
+
repos:
|
|
22
|
+
- repo: local
|
|
23
|
+
hooks:
|
|
24
|
+
- id: ruff-check
|
|
25
|
+
name: ruff check
|
|
26
|
+
entry: ruff check .
|
|
27
|
+
language: system
|
|
28
|
+
types: [python]
|
|
29
|
+
pass_filenames: false
|
|
30
|
+
|
|
31
|
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
32
|
+
rev: v5.0.0
|
|
33
|
+
hooks:
|
|
34
|
+
- id: end-of-file-fixer
|
|
35
|
+
exclude: ^(docs/(PHASE0_SPEC|AMENDMENTS)\.md$|pilot/(preregistration|gate|testsel)/)
|
|
36
|
+
- id: trailing-whitespace
|
|
37
|
+
exclude: ^(docs/(PHASE0_SPEC|AMENDMENTS)\.md$|pilot/(preregistration|gate|testsel)/)
|
|
38
|
+
- id: check-yaml
|
|
39
|
+
- id: check-added-large-files
|
|
40
|
+
# pilot/testsel/summary.{csv,json} are committed run artifacts (~64KB / ~22KB); the
|
|
41
|
+
# largest tracked *source* file in the repo is ~71KB (scripts/pb_calibration_probe.py).
|
|
42
|
+
# 500KB gives headroom without letting an actual accidental large-file commit through.
|
|
43
|
+
#
|
|
44
|
+
# pilot/preregistration/ is excluded rather than accommodated by raising the cap. It holds
|
|
45
|
+
# the §1 pre-registration evidence — the whole-Census candidate manifest of CI run
|
|
46
|
+
# 31910799023 (6.6 MB JSON + 4.5 MB CSV) and the frozen stratum list derived from it. Those
|
|
47
|
+
# are deliberately committed at full size: the pre-registration's claim is that a reader
|
|
48
|
+
# can re-derive every number in docs/PREREGISTRATION_STRATUM_LIST.md, and a truncated or
|
|
49
|
+
# externally hosted artifact cannot support it. Raising the cap instead would have
|
|
50
|
+
# disarmed the guard for the whole tree to admit two known files.
|
|
51
|
+
exclude: ^pilot/preregistration/
|
|
52
|
+
args: [--maxkb=500]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"title": "pbcheck: an auditor of pseudoreplication in single-cell RNA-seq differential expression",
|
|
3
|
+
"upload_type": "software",
|
|
4
|
+
"creators": [
|
|
5
|
+
{
|
|
6
|
+
"name": "Poliakov, Alexander"
|
|
7
|
+
}
|
|
8
|
+
],
|
|
9
|
+
"description": "pbcheck estimates how much an existing single-cell RNA-seq differential-expression analysis is inflated by pseudoreplication, treating individual cells as independent replicates when the true unit of replication is the donor. v0.1.0 is the single-stratum audit tool; Phase 0, a pre-registered measurement study, continues alongside it; its frozen protocol is in docs/PHASE0_SPEC.md and every deviation is recorded in docs/AMENDMENTS.md.",
|
|
10
|
+
"license": "bsd-3-clause",
|
|
11
|
+
"version": "0.1.0",
|
|
12
|
+
"keywords": [
|
|
13
|
+
"single-cell",
|
|
14
|
+
"scRNA-seq",
|
|
15
|
+
"differential-expression",
|
|
16
|
+
"pseudoreplication",
|
|
17
|
+
"pseudobulk",
|
|
18
|
+
"statistics",
|
|
19
|
+
"reproducibility"
|
|
20
|
+
],
|
|
21
|
+
"access_right": "open",
|
|
22
|
+
"related_identifiers": [
|
|
23
|
+
{
|
|
24
|
+
"identifier": "https://github.com/s4s4s4s/pbcheck",
|
|
25
|
+
"relation": "isSupplementTo",
|
|
26
|
+
"resource_type": "software"
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
}
|